Tuple works with nullptr but doesnt with NULL

Viewed 1243

I have the below code where std::tuple works with nullptr but not with NULL.

#include <iostream>
#include <tuple>

int main()
{
tuple<int*> t1, t2;

t1 = std::make_tuple(NULL);
t2 = std::make_tuple(nullptr);
}

When compiled with C++11, the code works when nullptr is used, but it gives the below error where NULL is used.

In file included from tuple.cpp:2:
/usr/lib/gcc/x86_64-linux-gnu/5.4.0/../../../../include/c++/5.4.0/tuple:447:8: error: assigning to 'int *' from incompatible type 'long'
            = std::forward<_UHead>(_Tuple_impl<_Idx, _UHead>::_M_head(__in));
              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/5.4.0/../../../../include/c++/5.4.0/tuple:575:36: note: in instantiation of function template specialization 'std::_Tuple_impl<0, int *>::operator=<long>' requested here
          static_cast<_Inherited&>(*this) = std::move(__in);
                                          ^
tuple.cpp:13:8: note: in instantiation of function template specialization 'std::tuple<int *>::operator=<long, void>' requested here
    t1 = std::make_tuple(NULL);
       ^
1 error generated.

Here, the type of NULL is long int and the tuple is strict enough and doesn't accept it.

How can we make it work using NULL as well, as our client says it works when used with nvcc compiler (when they use the above snippet in the CUDA code), but its not working otherwise.

4 Answers

How can we make it work using NULL as well

You can use either std::make_tuple<int*>(NULL) or simply std::tuple<int*>(NULL). But prefer using nullptr instead.

The problem that you face is the reason why nullptr was introduced to the language. Prior to its introduction, the only standard null pointer literal was 0 (also 0L etc. are valid), and that is what NULL expands1 to.

The problem is that 0 is not only a pointer literal, but also in fact an integer literal. And it is the integer nature of the literal that takes precedence in template type argument deduction and std::make_tuple(NULL) may result in std::tuple<int> or std::tuple<long> depending on exact definition of NULL. And those tuples aren't implicitly convertible to std::tuple<int*>.


1 Technically, now that nullptr is in the language, NULL could expand to that as well. But that's unlikely to happen.

One of the reasons why nullptr has its own type std::nullptr_t is that the literal 0 is special:

[conv.ptr]/1

A null pointer constant is an integer literal ([lex.icon]) with value zero or a prvalue of type std​::​nullptr_­t. A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type ([basic.compound]) and is distinguishable from every other value of object pointer or function pointer type. [...]

You can convert 0 (or 0l) to any pointer type, but you can't convert 1 (or 1l) or any other integer to any pointer type (without an explicit cast, that is).

This works fine as long as you define NULL as a macro for the literal (#define NULL 0l) and use it directly in places where this conversion is necessary. But this makes perfect forwarding impossible: A function (such as std::make_tuple) taking T&& will deduce the type as long when passed NULL, losing the special properties of the 0 literal.

As a solution, nullptr was created and given its own type std::nullptr_t, with implicit conversions like those of the 0 literal. This means that perfect forwarding of nullptr (but not NULL) preserves this "specialness".

Bottom line: Use nullptr instead of NULL (or 0) if you want to use C++11 features (in particular perfect forwarding) like std::make_X.

There is no "making it work". In a standard-conforming compiler (see [support.types.nullptr]/2 and footnote) using NULL with make_tuple will result in this exact problem. Don't use NULL or don't use make_tuple, that's the only answer to your question.

It seems that the used compiler defines the macro NULL as a null-pointer constant 0L.

So in this statement

t1 = std::make_tuple(NULL);

the type of the right expression is std::tuple<long>.

Try to run the following demonstrative program

#include <iostream>
#include <iomanip>
#include <tuple>
#include <type_traits>

int main() 
{
    auto t = std::make_tuple( NULL );

    std::cout << std::boolalpha 
              << std::is_same<std::tuple<long>, decltype( t )>::value << '\n';

    return 0;
}

Its output is

true

So the deduced parameter type of the tuple is long.

In fact this statement

t1 = std::make_tuple(NULL);

semantically is equivalent to the following code

#include <iostream>

int main() 
{
    int *p;
    long v = 0L;

    p = v;

    return 0;
} 

the compiler can issue an error similar to this

prog.cpp:8:6: error: invalid conversion from ‘long int’ to ‘int*’ [-fpermissive]
  p = v;
      ^

You need explicitly to cast NULL to the type int *. For example

t1 = std::make_tuple( ( int * )NULL);

Though in any case it is better to use nullptr. It is the reason why the pointer literal nullptr was introduced in C++.

Then you are using the pointer literal nullptr then it is implicitly can be converted to the type int *. In this case the assignment is correct. Consider

#include <iostream>

int main() 
{
    int *p;
    std::nullptr_t v = nullptr;

    p = v;

    return 0;
} 

Is it mandatory that t1 and t2 are objects of the same type?

a) case YES

t2 = std::make_tuple(static_cast<int*>(NULL));

b) case NO

std::tuple<decltype(NULL)> t2;
Related