Why cannot NULL be argument for std::thread's execution function's void* parameter?

Viewed 391

Here is a minimal case:

#include <iostream>
#include <functional>
#include <thread>
void fun(void *args) {}
int main() {
  std::thread thread_id(fun, NULL);
}

g++ compiles fail:

[firstlove@manjaro misc]$ g++ thread_and_shared_resources.cpp -lpthread -std=c++17
In file included from thread_and_shared_resources.cpp:3:
/usr/include/c++/9.3.0/thread: In instantiation of 'std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(void*); _Args = {long int}; <template-parameter-1-3> = void]':
thread_and_shared_resources.cpp:6:34:   required from here
/usr/include/c++/9.3.0/thread:120:44: error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues
  120 |           typename decay<_Args>::type...>::value,
      |                                            ^~~~~
/usr/include/c++/9.3.0/thread: In instantiation of 'struct std::thread::_Invoker<std::tuple<void (*)(void*), long int> >':
/usr/include/c++/9.3.0/thread:131:22:   required from 'std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(void*); _Args = {long int}; <template-parameter-1-3> = void]'
thread_and_shared_resources.cpp:6:34:   required from here
/usr/include/c++/9.3.0/thread:243:4: error: no type named 'type' in 'struct std::thread::_Invoker<std::tuple<void (*)(void*), long int> >::__result<std::tuple<void (*)(void*), long int> >'
  243 |    _M_invoke(_Index_tuple<_Ind...>)
      |    ^~~~~~~~~
/usr/include/c++/9.3.0/thread:247:2: error: no type named 'type' in 'struct std::thread::_Invoker<std::tuple<void (*)(void*), long int> >::__result<std::tuple<void (*)(void*), long int> >'
  247 |  operator()()
      |  ^~~~~~~~
[firstlove@manjaro misc]$ g++ thread_and_shared_resources.cpp -lpthread -std=c++17
In file included from thread_and_shared_resources.cpp:3:
/usr/include/c++/9.3.0/thread: In instantiation of 'std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(void*); _Args = {long int}; <template-parameter-1-3> = void]':
thread_and_shared_resources.cpp:6:34:   required from here
/usr/include/c++/9.3.0/thread:120:44: error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues
  120 |           typename decay<_Args>::type...>::value,
      |                                            ^~~~~
/usr/include/c++/9.3.0/thread: In instantiation of 'struct std::thread::_Invoker<std::tuple<void (*)(void*), long int> >':
/usr/include/c++/9.3.0/thread:131:22:   required from 'std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(void*); _Args = {long int}; <template-parameter-1-3> = void]'
thread_and_shared_resources.cpp:6:34:   required from here
/usr/include/c++/9.3.0/thread:243:4: error: no type named 'type' in 'struct std::thread::_Invoker<std::tuple<void (*)(void*), long int> >::__result<std::tuple<void (*)(void*), long int> >'
  243 |    _M_invoke(_Index_tuple<_Ind...>)
      |    ^~~~~~~~~
/usr/include/c++/9.3.0/thread:247:2: error: no type named 'type' in 'struct std::thread::_Invoker<std::tuple<void (*)(void*), long int> >::__result<std::tuple<void (*)(void*), long int> >'
  247 |  operator()()
      |  ^~~~~~~~

Why NULL cannot be passed here? the standalone call to fun with NULL works just fine:

fun(NULL) // compiles happily
2 Answers

NULL expands to 0 in your implementation, and 0 is an integer, not a pointer.
It is convertible to a pointer in some contexts but not in others; this is an example of the latter.

Use nullptr, it solves many similar issues.

TL;DR: NULL is legacy C++. In modern C++, use nullptr as a direct replacement for NULL to avoid issues like these.

Q: Why NULL cannot be passed here? the standalone call to fun with NULL works just fine

1) In glibc, NULL in C++ mode is #defined as 0L1

2) std::thread's constructor uses std::invoke to forward its arguments to the callable argument. When forwarding an rvalue 0L, it first of all becomes a variable (of a reference type).

3) The null pointer implicit conversion rule in C++ is specified in [conv.ptr]/1:

A null pointer constant is an integer literal 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 and is distinguishable from every other value of object pointer or function pointer type. Such a conversion is called a null pointer conversion.

That means C++ allows integral literals with a value of 0 (thus literally 0, 0L, 0u etc.), but not other integral values, to be implicitly convertible to a pointer type:

void fun(void* args) {}

int main() {

    fun(0); // OK

    int arg = 0;
    fun(arg); // error: invalid conversion from 'int' to 'void*'

}

Now since std::invoke is a template, the root cause error, "invalid conversion from 'long int' to 'void'*" in your case falls under SFINAE rules and is silently swallowed, leaving you with an obscure error about a problem with std::threads arguments.

The workaround is to use nullptr instead of NULL.


1 in C mode NULL is (void*)0, but that is prohibited in C++.

Related