60rntogo
60rntogo

Reputation: 211

C++20 concepts: how to refer to the class name in the `requires` clause?

I have a CRTP class

template <typename T>
class Wrapper
{
  // ...
};

that is intended to be derived as

class Type : Wrapper<Type>
{
  // ...
};

and I would like to enforce that by putting a constrain on the template parameter T. There is a friend trick that can accomplish that, but I figure that in the age of concepts there should be a better way. My first attempt was

#include <concepts>

template <typename T>
  requires std::derived_from<T, Wrapper<T>>
class Wrapper
{
  // ...
};

but this doesn't work since I'm referring to Wrapper before it was declared. I have found some workarounds that are not fully satisfactory. I can add the constraint to a constructor

Wrapper() requires std::derived_from<T, Wrapper<T>>;

but that is not convenient if I have more constructors that would have to be constrained as well. I can do it with the destructor

~Wrapper() requires std::derived_from<T, Wrapper<T>> = default;

but it feels a little silly to declare the destructor just to put requires on it.

I wonder if there is a better, more idiomatic way to do that. In particular, while these approaches seem to work (tested on gcc 10), one unsatisfying thing is that if I derive Type from Wrapper<OtherType>, then the error is raised only when I instantiate Type. Is it possible to have the error at the point of definition of Type?

Upvotes: 19

Views: 891

Answers (1)

Quimby
Quimby

Reputation: 19113

No, this is really not possible.

Right now it is a language problem - the name of the class does not exist before it is actually written in the code. But even if the C++ compilers read the file in multiple passes and knew the names, it would still not be enough. Allowing this would either require a major change of the type system, and not for the better, or it would be a very brittle feature at best. Let me explain.

Hypothetically if the name could be mentioned in the requires clause, the code would also fail because T=Type is still an incomplete type at this point. @Justin demonstrated that in his noteworthy comment my answer builds upon.

But to not make it end here and be a very boring version of "You are not allowed to do that" lets ask ourselves why is Me incomplete in the first place?

Take a look the following rather contrived example and see that knowing the full type of Me is impossible inside its base class.

#include <type_traits>

struct Foo;
struct Bar{};

template<typename T>
struct Negator {
    using type = std::conditional_t<!std::is_base_of_v<Foo,T>, Foo, Bar>;
};

struct Me: Negator<Me>::type{};

This is of course nothing else than C++ version of Russell's paradox which demonstrates that well-defined types/sets cannot be defined using themselves.

A simple question: is Foo a base class of Me? I.e. what is the value of std::is_base_of_v<Foo,Me>?

  • If it is not, the conditional in Negator is true and therefore Me is deriving from Negator<Me>::type i.e. Foo which is a contradiction.
  • On the other hand, if it does derive from Foo, we find out it actually does not.

It might seem like an artificial example and it is, you did ask about something else after all.

Yes, there probably is a finite number of paragraphs you could add to the Standard to allow that particular usage of your Wrapper and disallow my usage of Negator, but there would have to be drawn a very thin line between these not so dissimilar examples.

Another example of the need for early incompleteness before }; is the usage of sizeof which is probably a more commonly given argument:

  • sizeof(T) obviously depends on the size of all base classes. So using the expression inside a base class Wrapper of a derived type T that is still being written is another land mine waiting for you to step on.

  • Still, even easier example without any inheritance is:

    struct Me
    {
        int x[sizeof(Me)+1];
    };
    

    What is the size of Me?

The "friend trick"

I believe you are speaking about Prevent user from deriving from incorrect CRTP base. Yes, that works but for the same reason you putting requires near the methods worked. The constructor being deleted or inaccessible is checked only when its call is actually generated, which usually is only when an instance is created and at that point Me is a complete type.

This is also done for a good reason, you would want this code to work:

struct Me
{
    int size(){
        return sizeof(Me);
    }
};

Existence of a method cannot influence the type of Me, so this does not create any problems.

Upvotes: 5

Related Questions