Концепции С++ 20: как ссылаться на имя класса в предложении `requires`?

У меня есть класс CRTP

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

который предназначен для получения как

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

и я хотел бы обеспечить это, наложив ограничение на параметр шаблона T. Есть friend трюк, с помощью которого это можно сделать, но я полагаю, что в век концепций должен быть способ получше. Моя первая попытка была

#include <concepts>

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

но это не работает, так как я имею в виду Wrapper до того, как он был объявлен. Я нашел некоторые обходные пути, которые не полностью удовлетворительны. Я могу добавить ограничение в конструктор

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

но это неудобно, если у меня есть больше конструкторов, которые также должны быть ограничены. Я могу сделать это с помощью деструктора

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

но кажется немного глупым объявлять деструктор только для того, чтобы поставить на него requires.

Интересно, есть ли лучший, более идиоматический способ сделать это. В частности, хотя эти подходы, кажется, работают (проверено на gcc 10), одна неудовлетворительная вещь заключается в том, что если я получаю Type из Wrapper<OtherType>, то ошибка возникает только при создании экземпляра Type. Возможна ли ошибка в точке определения Type?


person 60rntogo    schedule 29.06.2020    source источник
comment
Даже static_assert(std::derived_from<T, Wrapper<T>>) после открывающей скобки в этом случае не работает, так как T неполный godbolt.org/z/ у-jVGZ   -  person Justin    schedule 30.06.2020


Ответы (1)


Нет, это действительно невозможно.

Сейчас это языковая проблема — имя класса не существует до того, как оно будет записано в коде. Но даже если бы компилятор C++ прочитал файл за несколько проходов и знал имена, этого все равно было бы недостаточно. Разрешить это либо потребовало бы существенного изменения системы типов и не в лучшую сторону, либо это было бы в лучшем случае очень хрупким решением. Позволь мне объяснить.

Гипотетически, если бы это имя могло быть упомянуто в предложении requires, код также потерпел бы неудачу, потому что T=Me на данный момент все еще является неполным типом. @Justin продемонстрировал, что в своем замечательном комментарии мой ответ основан на нем.

Но чтобы не заканчивать это здесь и не быть очень скучной версией Вам нельзя делать, давайте спросим себя, почему Me неполное?

Взгляните на следующий довольно надуманный пример и убедитесь, что узнать полный тип Me внутри его базового класса невозможно.

#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
{

};

Это, конечно, не что иное, как версия C++ парадокса Рассела, которая демонстрирует, что четко определенный объекты/наборы не могут быть определены с использованием самих себя.

Итак, какова ценность std::is_base_of_v<Foo,Me>? То есть Me происходит от Foo?

Если это не так, то в этом случае условие в классе Negator истинно, и, таким образом, Me происходит от Negator<Me>::type, то есть Foo, что является противоречием.

С другой стороны, если оно происходит от Foo, мы обнаруживаем, что на самом деле это не так.

Это может показаться искусственным примером, и, в конце концов, вы спросили о чем-то другом. Да, вероятно, существует конечное число абзацев, которые вы могли бы добавить в Стандарт, чтобы разрешить конкретное использование вашего Wrapper и запретить мое использование Negator, но между этими не столь уж несходными примерами должна быть проведена очень тонкая грань.

Эта потребность в ранней завершенности до того, как }; сломает sizeof, что, вероятно, является более распространенным аргументом:

  • sizeof(Me) очевидно зависит от размера всех базовых классов. Таким образом, использование выражения внутри базового класса, который все еще пишется, а потому не может быть полным по определению и не иметь размера, — это еще одна мина, ожидающая, когда вы наступите на нее.

  • Тем не менее, еще более простой пример:

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

Трюк с другом

Я полагаю, вы говорите об этом этом. Да, это работает, но по той же причине, по которой вы поставили requires рядом с методами, которые сработали. Удаляемый или недоступный конструктор проверяется только тогда, когда его вызов действительно сгенерирован, что обычно происходит только тогда, когда создается экземпляр, и в этот момент Me является полным типом.

Это также сделано по уважительной причине, вы бы хотели, чтобы этот код работал:

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

Метод не может влиять на тип Me, так что это не создает никаких проблем.

person Quimby    schedule 07.12.2020