Search code examples
c++templatesc++17

What is the difference between passing NULL vs. nullptr to a template parameter?


I'm dealing with a fairly uniform vendor-provided API, and would like to check -- and handle -- any failures in a unified fashion too. To that end, I wrote the following wrapper:

template <typename Func, typename... Args>
auto awrap(Func &func, Args&&... args)
{
    auto code = func(args...);

    if (code >= 0)
        return code;

    ... handle the error ...
};
...
awrap(handlepath, handle, path, NULL, 0, coll, NULL);

The above compiles fine with clang, but g++13 -- and Microsoft VC++ -- both complain about the two NULL-arguments:

... error: invalid conversion from 'int' to 'const char*' [-fpermissive]
   87 |                 auto code = func(args...

Replacing the two NULLs with nullptr solves the problem, but why does it matter?

Likely, the NULL is turned into 0x0 or even 0 somewhere by the preprocessor, but the original call never raised an "eyebrow". Using NULL was perfectly suitable in:

handlepath(handle, path, NULL, 0, coll, NULL);

Why is it a problem (for some compilers), when used in a wrapper?


UPDATE: the /usr/include/sys/_null.h on my FreeBSD system has the following code:

#ifndef NULL

#if !defined(__cplusplus)
#define NULL    ((void *)0)
#else
#if __cplusplus >= 201103L
#define NULL    nullptr
#elif defined(__GNUG__) && defined(__GNUC__) && __GNUC__ >= 4
#define NULL    __null
#else
#if defined(__LP64__)
#define NULL    (0L)
#else
#define NULL    0
#endif  /* __LP64__ */
#endif  /* __GNUG__ */
#endif  /* !__cplusplus */

#endif

So:

  1. for C, NULL is (void *)0;

  2. for clang++ NULL and nullptr are the same thing, whereas for GNU it may not be...


Solution

  • In many implementations, NULL is just a #define for an integer literal 0, eg:

    #define NULL 0
    

    You can directly assign a literal 0 to any pointer, which is why passing NULL directly to the target function works fine.

    Since you are passing NULL to a template parameter, that parameter is being deduced as type int, as the error message says.

    In this call:

    awrap(handlepath, handle, path, NULL, 0, coll, NULL);
    

    awrap() will resolve to something like this:

    auto awrap(decltype(handlepath) &func, decltype(handle) arg1, decltype(path) arg2,
        int arg3, int arg4, decltype(coll) arg5, int arg6)
     // ^^^^^^^^                                 ^^^^^^^^
    {
        auto code = func(arg1, arg2, arg3, arg4, arg5, arg6);
        //                    error: ^^^^       error: ^^^^
        ...
    };
    

    To assign an int variable to a pointer, you need an explicit type-cast, which you are not using.

    Whereas nullptr is of type nullptr_t, which can also be assigned directly to any pointer. There is only 1 possible value of nullptr_t. So, when passing nullptr to a template parameter, that parameter will be deduced as type nullptr_t. And the compiler knows how to assign a nullptr_t to a pointer.

    In this call:

    awrap(handlepath, handle, path, nullptr, 0, coll, nullptr);
    

    awrap() will resolve to something like this:

    auto awrap(decltype(handlepath) &func, decltype(handle) arg1, decltype(path) arg2,
        nullptr_t arg3, int arg4, decltype(coll) arg5, nullptr_t arg6)
     // ^^^^^^^^^^^^^^                                 ^^^^^^^^^^^^^^
    {
        auto code = func(arg1, arg2, arg3, arg4, arg5, arg6);
        //                       OK: ^^^^          OK: ^^^^
        ...
    };
    

    So, when calling your template, you should use nullptr.

    But, if you want to use NULL then you must type-cast it as (char*)NULL (or equivalent), or whatever other pointer type the parameter is expecting, eg:

    awrap(handlepath, handle, path, (char*)NULL, 0, coll, (char*)NULL);