c++language-lawyerc++20copy-constructorc++-concepts

Copy constructor with impossible requires-constraint


In the following program, the copy constructor of struct S is declared with the constraint that the class is not copy constructible:

#include <concepts>

template <typename T>
struct S {
    S() = default;
    S(const S &) requires (!std::copy_constructible<S>) {}
};

S<int> u;
S<int> v(u); //copy construction

static_assert( !std::copy_constructible<S<int>> );

I expected that the program would fail with an error, something like "constraint satisfaction depends on itself". And indeed, MSVC fails it, but with a rather obscure error message:

<source>(6): error C7608: atomic constraint should be a constant expression
<source>(6): note: the template instantiation context (the oldest one first) is
<source>(6): note: while evaluating concept 'copy_constructible<S<int> >'
Z:/compilers/msvc/14.40.33807-14.40.33811.0/include\concepts(170): note: while evaluating concept 'move_constructible<S<int> >'
Z:/compilers/msvc/14.40.33807-14.40.33811.0/include\concepts(105): note: while evaluating concept 'constructible_from<S<int>,S<int> >'
<source>(6): error C2131: expression did not evaluate to a constant
<source>(6): note: failure was caused by a read of an uninitialized symbol
<source>(6): note: see usage of 'std::copy_constructible<S<int>>'

But, both GCC and Clang successfully build the program without any warnings.

Online demo: https://gcc.godbolt.org/z/vTYnEGGva

Which implementation is correct here?


Solution

  • The C++20/23 standard says that using a trait or concept on an incomplete type is undefined if completing that type could change the result. That leads to a pretty neat edge case:

    // 1) This lives before you ever define any S<int> objects:
    static_assert(std::is_copy_constructible_v<S<int>>,
                  "S<int> should not be copy-constructible according to type_traits");
    
    // 2) And *after* that you check the concept:
    static_assert(!std::copy_constructible<S<int>>,
                  "S<int> should not satisfy std::copy_constructible");
    
    // 3) Then you define some S<int> objects:
    S<int> u;
    S<int> v(u);  // copy construction
    
    

    If you put the is_copy_constructible check above the concept, it compiles—because the trait runs in an unevaluated, SFINAE-based context on an incomplete type. But if you place the copy_constructible first , it fails, since the concept does a real requires-check on the now-complete class. It’s a small example of how traits and concepts differ when you poke at incomplete types.