How can I use "nullptr" for null pointers prior to C23?

777 views Asked by At

In C23, the nullptr keyword got standardized. I would prefer to use nullptr instead of NULL prior to C23 too because it means that I could write code which compiles in:

  • C, prior to C23
  • C, since C23
  • C++

I could simply use NULL in both languages and every C standard, but that would be highly unusual in C++ code, and nullptr is becoming the norm in both languages anyway.

As far as I know, you are not allowed to use #define to replace keywords in the language, and that may cause problems when defining a compatibility macro. Basically, I need:

// only if this is neither C++ nor C23
#define nullptr /* something */

How can I properly define this macro?

3

There are 3 answers

9
Lundin On BEST ANSWER

Some things of note:

  • __STDC__ and __STDC_VERSION__ were added in "C95" (the addendum to ISO C 9899:1990).
  • You cannot check for nullptr using pre-processor conditionals because it is not a macro.
  • The user might forget to include stddef.h or equivalent header even when using a conforming C23 compiler so don't assume that this one is #included.
  • Current versions of gcc and clang support -std=c2x which does contain nullptr but sets __STDC_VERSION__ to a placeholder value 202000L.
  • Not all versions of C++ contain nullptr since it was added in C++11.

Therefore the macro checks should look something like this:

/* C++11 or later? */
#if (defined(__cplusplus) && __cplusplus >= 201103L)  
  #include <cstddef>

/* C2x/C23 or later? */
#elif ( defined(__STDC__) &&          \
        defined(__STDC_VERSION__) &&  \
        (__STDC_VERSION__ >= 202000L) )
  #include <stddef.h> /* nullptr_t */

/* pre C23, pre C++11 or non-standard */
#else
  #define nullptr (void*)0
  typedef void* nullptr_t;

#endif
18
Jan Schultke On

It is possible to detect whether you're using C23 through predefined macros:

/* bad, don't do this */
#if !__cplusplus && __STDC_VERSION__ <= 201710
    #define nullptr ((void*)0)
#endif

However, such a simple macro can trigger compiler warnings (clang -Wundef -std=c89):

<source>:1:6: warning: '__cplusplus' is not defined, evaluates to 0 [-Wundef]
    1 | #if !__cplusplus && __STDC_VERSION__ <= 201710
      |      ^
<source>:1:21: warning: '__STDC_VERSION__' is not defined, evaluates to 0 [-Wundef]
    1 | #if !__cplusplus && __STDC_VERSION__ <= 201710
      |  

The solution can be rewritten so no warnings are triggered:

/* don't do anything in C++, or if someone else defined a compatibility macro */
#if !defined(__cplusplus) && !defined(nullptr) && \
   (!defined(__STDC_VERSION__) || __STDC_VERSION__ <= 201710)
    /* -Wundef is avoided by using short circuiting in the condition */
    #define nullptr ((void*)0)
#endif

See live example at Compiler Explorer

0
John Bollinger On

As far as I know you are not allowed to use #define to replace keywords in the language, and that may cause problems when defining a compatibility macro.

Mostly true (C23 6.4.2.1/6). There is an explicit exception for keywords whose names begin with a double-underscore or with an underscore and a capital letter. (C23 6.4.2.1/7, as clarified by footnote 80)

Although "not allowed" is not how I would describe it, at least for C. The language spec does not forbid defining a macro whose identifier matches a language keyword of any form. But, subject to the exception described above, it will interpret language keywords as keywords, which are not subject to macro replacement, rather than as identifiers.

I would prefer to use nullptr instead of NULL prior to C23 too because it means that I could write code which compiles in [C++ and all versions of standard C].

This is a noble goal, and, speaking from (pre-C23) experience, a true PITA. The shared subset of C and C++ is large enough to program in, but it is not particularly comfortable. I would suggest that you choose one language to write in, and support the other via binary compatibility instead of source compatibility.

But if you insist, then your other answer shows how you can use language version macros to inform conditional compilation directives, so as to make appropriate decisions about whether to define a nullptr macro.

On the other hand, as I already covered, it is allowed in C to define a macro with the same name as a keyword -- it is just ineffective. So you could consider just defining nullptr as a macro unconditionally, or at least for all versions of C. A C23 compiler might be moved to warn about that, but a conforming one should accept it.