4

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?

3
  • 2
    I'd be very surprised if this isn't IFNDR, considering the self-dependency can be buried in arbitrary complex constexpr code. Commented May 16 at 22:24
  • 1
    I'm not sure whether this applies, but it looks relevant to me: eel.is/c++draft/res.on.functions "the behavior is undefined... if an incomplete type is used as a template argument when instantiating a template component or evaluating a concept, unless specifically allowed for that component." Commented May 16 at 22:35
  • 1
    Just to clarify, the injected-class-name in the member function's constraint is indeed an incomplete type when the concept is evaluated with it as a template argument. Commented May 16 at 22:38

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.