5

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?

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • Have you tried #ifndef? – stark Aug 14 '23 at 14:21
  • @stark I'd expect the concern here is it's not a `#define` but a new keyword. – tadman Aug 14 '23 at 14:22
  • There's no way to comparatively `define` `nullptr` because `nullptr` is a keyword, not a value, right? – Fiddling Bits Aug 14 '23 at 14:22
  • I don't see what's wrong with `0`; this `nullptr` only confuses the issue, especially hidden behind a macro. – Neil Aug 14 '23 at 15:26
  • @Neil `0` or `NULL` wouldn't be very idiomatic in C++. They may also bypass constructors that are meant to catch mistakes, like `std::string(std::nullptr_t)`. Now that `nullptr` is a keyword in both languages, I think it should be the default, and used in all code. The question is just how to define a fallback macro for C/C++ versions where it's not a keyword yet. – Jan Schultke Aug 14 '23 at 15:30
  • I think allowing `C` and `C++` to diverge should have happened years ago, but the decisions of standard committees aside, still a very valid question. – Neil Aug 14 '23 at 15:52

3 Answers3

8

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
Lundin
  • 195,001
  • 40
  • 254
  • 396
  • I would strongly recommend avoiding `NULL`, and instead using an explicit `((void*)0)`. A conforming C implementation could define `NULL` such that it expands to an integer (which, of course, does not preclude the expansion from being a null pointer constant). Else you might have the confusing situation where the type of `nullptr` is different from `nullptr_t`. – John Bollinger Aug 14 '23 at 17:26
  • @JohnBollinger `NULL` is guaranteed to be a _null pointer constant_, which in turn means either `(void*)0` or `0`. `NULL` cannot be anything else but those two. (6.3.2.3 §3 and 7.19) Not to be confused with the representation of a _null pointer_, which is what you get if you assign a null pointer constant to a pointer. During that assignment, the compiler is free to give the pointer any implementation-defined value including non-zero values. – Lundin Aug 15 '23 at 06:16
  • More or less, @Lundin. `NULL` can be *any* integer constant expression with value 0. But perhaps I was unclear. I thought it would be understood that I was not recommending avoiding `NULL` *in general*, but only to avoid using it to define `nullptr`. If the implementation opts for the integer constant expression option then `NULL` has type `int` (notwithstanding that it is a null pointer constant). If you then use that define `nullptr`, and also define `nullptr_t` as `void *`, then the type of `nullptr` is different from `nullptr_t`. Don't you think that's undesirable? – John Bollinger Aug 15 '23 at 11:28
  • 1
    @JohnBollinger Yeah if you are doing this for the purpose of avoiding `0` and not just for compatibility reasons. I guess an integer constant expression would be undesirable in cases like `_Generic(x, int: foo(), nullptr_t: bar());` – Lundin Aug 15 '23 at 11:58
  • Yes, that's a specific case that could cause *bona fide* issue. More generally, those versions of C and C++ that provide `nullptr` and `nullptr_t` all specify that the latter is the type of the former, and code aiming at those versions is entitled to rely on that. The OP is looking for compatibility, so needlessly breaking that type relationship is counterproductive for them. – John Bollinger Aug 15 '23 at 12:22
  • clang actually protests against `(nullptr_t){0}` in C23 mode. Fun little example: https://godbolt.org/z/j6YKx8osq. Won't compile with clang in strict mode. – Lundin Aug 15 '23 at 12:24
  • 2
    @JohnBollinger Ok so you convinced me :) I updated the code to use `(void*)0` instead. – Lundin Aug 15 '23 at 12:25
  • 1
    As for Clang, I think it's very reasonable in this respect. The spec says "No type other than `nullptr_t` shall be converted to `nullptr_t`" (C23 6.5.4/4). The fact that `0` is a null pointer constant does not get around that. In fact, you can't convert `(void *)0` to `nullptr_t`, either, unless by leveraging an extension. – John Bollinger Aug 15 '23 at 12:29
  • 2
    @JohnBollinger Makes me wonder how the compiler is supposed to behave if you union type pun a pointer with a given value into a `nullptr_t`. Probably a lot of strange corner cases that the ISO WG didn't quite forsee :) – Lundin Aug 15 '23 at 12:50
6

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

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • 2
    You can also use `#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 202311L` which doesn't trigger warnings either. – Nate Eldredge Aug 14 '23 at 14:36
  • 4
    `#if !__cplusplus` is always naive and dangerous, should be `#if !defined(__cplusplus)`. Otherwise a macro which is not defined evaluates to 0 and `!0` gives 1. – Lundin Aug 14 '23 at 14:37
  • @Lundin, ...which works as intended. Not sure what your point is. – ikegami Aug 14 '23 at 14:39
  • And `nullptr` is not a macro but a predefined constant. – Lundin Aug 14 '23 at 14:39
  • @ikegami Well to begin with not all versions of C++ support nullptr. – Lundin Aug 14 '23 at 14:40
  • @Lundin Switching to `!defined(__cplusplus)` is not going to help that. Not sure what your point here is either. – ikegami Aug 14 '23 at 14:41
  • @ikegami No. `nullptr` is not a macro so it cannot get passed to `defined`. – Lundin Aug 14 '23 at 14:44
  • Thanks @NateEldredge, that makes the solution a bit shorter; I've updated the answer. – Jan Schultke Aug 14 '23 at 14:45
  • 1
    Also as of today both gcc and clang support nullptr_t but they do not support `__STDC_VERSION__ == 202311L` because that constant is set to `202000L` when compilding with -std=c2x. – Lundin Aug 14 '23 at 14:49
  • You might also want to conditionally define the type `nullptr_t` which is the type of `nullptr`. For C23 it gets defined by `#include ` as `typedef typeof_unqual(nullptr) nullptr_t;` but you'd probably want `typedef void *nullptr_t;` for earlier versions. – Ian Abbott Aug 14 '23 at 14:52
  • 1
    To spell it out, `defined(nullptr)` is always 0 on all compilers made to this date and will still be 0 when C23 is released. – Lundin Aug 14 '23 at 14:53
  • @Lundin good point; I've made it detect whether it's C17 or lower instead. – Jan Schultke Aug 14 '23 at 14:54
  • 1
    @Lundin my concern is that someone else could have had the same bright idea in C17 or lower, and they would have defined `nullptr` themselves. In that case, `#define` would overwrite an existing macro and give you a warning. – Jan Schultke Aug 14 '23 at 14:55
  • @IanAbbott I'm not sure if that's a good idea. `nullptr_t` is intended to be unique type, and this property could be relied upon in generic selection etc.. If someone defined it as a non-unique type, it could break stuff. The best solution is probably not to use `nullptr_t` prior to C23 at all, to ensure compatibility. – Jan Schultke Aug 14 '23 at 14:57
  • Why `#define nullptr ((void*)0)` instead of `#define nullptr NULL`? – ad absurdum Aug 14 '23 at 15:02
  • 1
    @adabsurdum `NULL` could have integer type in C. – Ian Abbott Aug 14 '23 at 15:03
  • @JanSchultke Good point! I neglected the part about `nullptr_t` needing to be a unique pointer type. – Ian Abbott Aug 14 '23 at 15:06
  • 1
    @adabsurdum because I don't want to assume that the user included `` or some other header which includes `NULL`. I also don't want to include such a header if it's not really necessary. – Jan Schultke Aug 14 '23 at 15:12
2

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.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157