admin管理员组

文章数量:1122832

The major C++ implementations seems to have quite a different idea about this. What to make of the validity of the following 4 assertions?

struct a {
    template<typename T = void>
    constexpr a() { (T&&)0; }
};
struct b : a {};

template<typename T = void>
constexpr int f() { return (T&&)0; }

template<typename T = void>
constexpr int v = (T&&)0;

static_assert(sizeof(a{})); // #1 - clang ok,   gcc nope, msvc ok
static_assert(sizeof(b{})); // #2 - clang nope, gcc ok,   msvc ok
static_assert(sizeof(f())); // #3 - clang ok,   gcc nope, msvc ok
static_assert(sizeof(v<>)); // #4 - clang ok,   gcc ok,   msvc nope

Obviously, forming a reference to void (i.e. (T&&)0 where T = void) is not a valid expression. However, such an expression is never actually evaluated in this example program, as it's only part of the definition of an unevaluated operand. What really explains this divergence in behavior amongst all of these compilers?

Demo


Clang's error message:

<source>:3:23: error: cannot form a reference to 'void'
    3 |     constexpr a() { (T&&)0; }
      |                       ^
<source>:14:24: note: in instantiation of function template specialization
'a::a<void>' requested here
   14 | static_assert(sizeof(b{}));
      |                        ^

GCC's error message:

<source>: In instantiation of 'constexpr a::a() [with T = void]':
<source>:13:24:   required from here
   13 | static_assert(sizeof(a{}));
      |                        ^
<source>:3:21: error: forming reference to void
    3 |     constexpr a() { (T&&)0; }
      |                     ^~~~~~
<source>: In instantiation of 'constexpr int f() [with T = void]':
<source>:15:23:   required from here
   15 | static_assert(sizeof(f()));
      |                     ~~^~~
<source>:8:28: error: forming reference to void
    8 | constexpr int f() { return (T&&)0; }
      |                            ^~~~~~

MSVC's error message:

<source>(11): error C7683: you cannot create a reference to 'void'
<source>(11): note: the template instantiation context (the oldest one first) is
<source>(16): note: see reference to variable template 'const int v<void>'
being compiled

The major C++ implementations seems to have quite a different idea about this. What to make of the validity of the following 4 assertions?

struct a {
    template<typename T = void>
    constexpr a() { (T&&)0; }
};
struct b : a {};

template<typename T = void>
constexpr int f() { return (T&&)0; }

template<typename T = void>
constexpr int v = (T&&)0;

static_assert(sizeof(a{})); // #1 - clang ok,   gcc nope, msvc ok
static_assert(sizeof(b{})); // #2 - clang nope, gcc ok,   msvc ok
static_assert(sizeof(f())); // #3 - clang ok,   gcc nope, msvc ok
static_assert(sizeof(v<>)); // #4 - clang ok,   gcc ok,   msvc nope

Obviously, forming a reference to void (i.e. (T&&)0 where T = void) is not a valid expression. However, such an expression is never actually evaluated in this example program, as it's only part of the definition of an unevaluated operand. What really explains this divergence in behavior amongst all of these compilers?

Demo


Clang's error message:

<source>:3:23: error: cannot form a reference to 'void'
    3 |     constexpr a() { (T&&)0; }
      |                       ^
<source>:14:24: note: in instantiation of function template specialization
'a::a<void>' requested here
   14 | static_assert(sizeof(b{}));
      |                        ^

GCC's error message:

<source>: In instantiation of 'constexpr a::a() [with T = void]':
<source>:13:24:   required from here
   13 | static_assert(sizeof(a{}));
      |                        ^
<source>:3:21: error: forming reference to void
    3 |     constexpr a() { (T&&)0; }
      |                     ^~~~~~
<source>: In instantiation of 'constexpr int f() [with T = void]':
<source>:15:23:   required from here
   15 | static_assert(sizeof(f()));
      |                     ~~^~~
<source>:8:28: error: forming reference to void
    8 | constexpr int f() { return (T&&)0; }
      |                            ^~~~~~

MSVC's error message:

<source>(11): error C7683: you cannot create a reference to 'void'
<source>(11): note: the template instantiation context (the oldest one first) is
<source>(16): note: see reference to variable template 'const int v<void>'
being compiled
Share Improve this question edited Nov 23, 2024 at 9:19 303 asked Nov 23, 2024 at 0:13 303303 4,6421 gold badge15 silver badges34 bronze badges 5
  • Doesn't "3 compilers give 3 results" kind of guarantee that this is undefined behaviour? – Andras Deak -- Слава Україні Commented Nov 23, 2024 at 0:17
  • 3 @AndrasDeak--СлаваУкраїні Not necessarily. Unspecified and implementation-defined behaviors are also possible. – jamesdlin Commented Nov 23, 2024 at 0:21
  • 3 And so are compiler bugs. – Nate Eldredge Commented Nov 23, 2024 at 0:50
  • 2 And so are bugs in the standard itself. – n. m. could be an AI Commented Nov 23, 2024 at 6:22
  • 2 Sometimes the compilers aren't even necessarily "buggy", they just aren't yet caught up on a core issue resolution that was issued recently (or not so recently) – Brian Bi Commented Nov 23, 2024 at 15:49
Add a comment  | 

1 Answer 1

Reset to default 2

The a{}, f(), and v<> examples are definitely well-formed under [temp.inst]/5 and [temp.inst]/6:

Unless a function template specialization is a declared specialization, the function template specialization is implicitly instantiated when the specialization is referenced in a context that requires a function definition to exist or if the existence of the definition affects the semantics of the program. [...]

Unless a variable template specialization is a declared specialization, the variable template specialization is implicitly instantiated when it is referenced in a context that requires a variable definition to exist or if the existence of the definition affects the semantics of the program. [...]

Consider this version of the program:

struct a {
    template<typename T = void>
    constexpr a();
};
struct b : a {};

template<typename T = void>
constexpr int f();

template<typename T = void>
extern const int v;

static_assert(sizeof(a{}));
static_assert(sizeof(b{}));
static_assert(sizeof(f()));
static_assert(sizeof(v<>));

template <typename T>
constexpr int v = (T&&)0;

int main() {}

a::a is no longer defined; same for f, and that makes GCC happy; it doesn't complain about a missing definition or anything, because asking for sizeof(a{}) or sizeof(f()) can be computed based on just the declarations. For the v case, we aren't actually allowed to forward-declare it with constexpr, but we can declare it const and define it with constexpr later. With this version, or with the declaration alone, MSVC stops complaining. So MSVC and GCC have bugs in this area where they instantiate definitions too eagerly.

The b case is the most interesting. In order for the compiler to check whether sizeof(b{}) is well-formed, it has to check whether b's default constructor is deleted. In Clang's case this appears to result in generating a "full definition" of b::b (I use scare quotes because technically = default; is already a definition—in the language of the standard, but not as we would typically think of it); in other words, it's as if the definition b() {} were generated and since that function calls a::a it means that a::a must be instantiated. GCC is not as heavy-handed; it's able to figure out that b::b is non-deleted, without actually going any further and forcing the instantiation of a::a. However, the standard is not clear about which behavior is correct.

本文标签: cDo unevaluated template operands need to have valid defintionsStack Overflow