Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Templated delegating copy constructor in constant expressions

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?
like image 792
xskxzr Avatar asked Mar 01 '20 05:03

xskxzr


People also ask

Can copy constructor be virtual in C++?

No you can't, constructors can't be virtual.

Is copy constructor default in C++?

Default Copy Constructors: When a copy constructor is not defined, the C++ compiler automatically supplies with its self-generated constructor that copies the values of the object to the new object.

What is the purpose of copy constructor in C++?

The copy constructor is used to initialize the members of a newly created object by copying the members of an already existing object. 2. Copy constructor takes a reference to an object of the same class as an argument.

Which options are valid for calling a copy constructor?

In C++, a Copy Constructor may be called for the following cases: 1) When an object of the class is returned by value. 2) When an object of the class is passed (to a function) by value as an argument. 3) When an object is constructed based on another object of the same class.


1 Answers

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.

like image 171
ecatmur Avatar answered Oct 28 '22 18:10

ecatmur