6

This question is motivated by this one.

Consider the following code:

struct B {};

struct S {
    B b; // #1

    S() = default;

    template <typename ...dummy> // #2
    constexpr S(const S&) {}

    template <typename ...dummy> // #3
    constexpr S(S &other) 
        : S(const_cast<const S&>(other)) // #4
    {}
};

S s;
constexpr S f() {return s;}

int main() {
    constexpr auto x = f();
}

GCC compiles this code successfully, but Clang rejects it (Example on Godbolt.org). The error message produced by Clang is

<source>:21:20: error: constexpr variable 'x' must be initialized by a constant expression
    constexpr auto x = f();
                   ^   ~~~
<source>:13:11: note: read of non-constexpr variable 's' is not allowed in a constant expression
        : S(const_cast<const S&>(other)) 
          ^
<source>:13:11: note: in call to 'S(s)'
<source>:18:25: note: in call to 'S(s)'
constexpr S f() {return s;}
                        ^
<source>:21:24: note: in call to 'f()'
    constexpr auto x = f();
                       ^
<source>:17:3: note: declared here
S s;
  ^

Note if we remove any of #2, #3 or #4, both compilers accept this code. If we replace #1 with int b = 0;, both compilers reject it.

My question is:

  1. Which compiler is correct according to the current standard?
  2. If GCC is correct, why does replacing #1 with int b = 0; make this code ill-formed? If Clang is correct, why does removing any of #2, #3 or #4 make this code well-formed?
xskxzr
  • 12,442
  • 12
  • 37
  • 77
  • 4
    BTW, none of those functions are copy constructors. By definition, a constructor is a copy constructor only if it is not a template. – Nicol Bolas Mar 01 '20 at 06:45
  • @NicolBolas Thanks for pointing out this. I only use "copy constructor" in the title to summarize the problem. If you have a better title, please feel free to edit it. – xskxzr Mar 01 '20 at 11:50

1 Answers1

3

Since both your user-defined constructors are templates, they are not copy (or move) constructors. So the compiler implicitly declares a copy constructor, and defines it as defaulted.

Part 1 thus comes down to the following distinguishing program:

struct A {
    struct B {} b;
    constexpr A() {};
    // constexpr A(A const& a) : b{a.b} {}    // #1
};
int main() {
    auto a = A{};
    constexpr int i = (A{a}, 0);
}

Rejected by Clang and MSVC, accepted by gcc; uncomment #1 for all three to accept.

Per the definition of the implicitly-defined copy constructor there is no way that #1 is any different to constexpr A(A const&) = default; so gcc is correct. Note also that if we give B a user-defined constexpr copy constructor Clang and MSVC again accept, so the issue appears to be that these compilers are unable to track the constexpr copy constructibility of recursively empty implicitly copyable classes. Filed bugs for MSVC and Clang (fixed for Clang 11).

Part 2:

Removing #1 means that you are copying (performing lvalue-to-rvalue conversion on) an object s.b of type int, whose lifetime began outside constexpr context.

Removing #2 gives S a user-defined constexpr copy constructor, which is then delegated to at #4.

Removing #3 gives S a user-defined (non-const) copy constructor, suppressing the implicitly-defined copy constructor, so the delegating construction invokes the template const constructor (which, remember, is not a copy constructor).

Removing #4 means that your constructor template with argument S& other no longer calls the implicitly-defined copy constructor, so b is default-initialized, which Clang can do in constexpr context. Note that a copy constructor is still implicitly declared and defined as defaulted, it is just that your constructor template<class...> S::S(S& other) is preferred by overload resolution.

It is important to recognize the distinction between suppressing the implicitly-defined copy constructor and providing a preferred overload. template<class...> S::S(S&) does not suppress the implicitly-defined copy constructor, but it is preferred for non-const lvalue argument, assuming that the implicitly-defined copy constructor has argument S const&. On the other hand, template<class...> S::S(S const&) does not suppress the implicitly-defined copy constructor, and can never be preferred to the implicitly-defined copy constructor since it is a template and the parameter-lists are the same.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Could you please explain more on "recursively empty implicitly copyable classes", what do you mean by "recursively empty"? – xskxzr Mar 03 '20 at 02:07
  • @xskxzr by "recursively empty" I mean a non-polymorphic class type all of whose non-static data members and immediate bases are recursively empty. In other words there are no scalar objects anywhere within it, so its value representation is empty - it is stateless. By "implicitly copyable" I mean that its copy constructor is not user-supplied, and all its immediate bases are implicitly copyable, so its copy constructor does not invoke any user-supplied code. (1/2) – ecatmur Mar 03 '20 at 10:36
  • Put these together and you have a class whose copy constructor is a total no-op, so should be OK to invoke in constexpr context even if the source object's lifetime began outside the context. (2/2) – ecatmur Mar 03 '20 at 10:37