Можно ли проверить концепцию на неполный тип

#c #c 20 #c -concepts

#c #c 20 #c -концепции

Вопрос:

Я наткнулся на это:

 #include <type_traits>
#include <concepts>

template<class T>
concept IsFoo = requires(T a)
{
    {a.a} -> std::same_as<int>;
};

#if 1
// Will not compile, because Foo currently has incomplete type
template<IsFoo AFoo>
struct AcceptsFoo
{};
#else
template<class AFoo>
struct AcceptsFoo
{};
#endif

struct Foo
{
    int a;
    int b;

    AcceptsFoo<Foo> obj;
};
  

https://gcc.godbolt.org/z/j43s4z

Другой вариант (crtp) https://gcc.godbolt.org/z/GoWfhq

Foo является неполным, потому что он должен создать экземпляр AcceptsFoo , но для этого Foo должен быть полным, иначе он не сможет проверить IsFoo . Это ошибка в GCC, или так сказано в стандарте? Последнее было бы печально, потому что это предотвращает совместное использование концепций с некоторыми хорошо известными шаблонами, такими как CRTP.

Я заметил, что clang выдает аналогичную ошибку:https://gcc.godbolt.org/z/d5bEez

Ответ №1:

Вы можете проверить концепцию на соответствие неполному типу, но если для этой проверки концепции фактически требуется что-либо сделать с типом, который требует ее завершения, это приведет к провалу проверки концепции.

И даже там вы должны быть осторожны, поскольку реализациям разрешено (и они будут) кэшировать проверки концепции для более быстрой компиляции — поэтому, если вы попытаетесь, C<T> пока T не завершено, и повторите попытку, когда T станет полным, и они должны дать разные ответы, вы напрашиваетесь на неприятности.

Foo на момент проверки она не завершена, поэтому, естественно, у нее нет имени элемента a . Это действительно не может сработать.

потому что это предотвращает совместное использование концепций с некоторыми хорошо известными шаблонами, такими как CRTP.

Используется вместе таким образом, да. Точно так же, как с CRTP вы также не можете получить доступ ни к чему непосредственно из параметра шаблона, переданного в базовый класс, вы всегда должны быть осторожны, чтобы отложить любое создание экземпляра этого типа до его завершения.

В конечном счете, это одна и та же причина:

 template <typename Derived>
struct B { 
    typename Derived::type x;
};

struct D : B<D> {
    using type = int;
};
  

не работает.

Комментарии:

1. «таким образом, естественно , у него нет имени элемента a » (курсив мой). Для некоторых частей (возможно, действительно только в определении класса), «под определением» можно увидеть часть, уже определенную (но не позже) (мне уже приходилось перемещать элементы снизу вверх, чтобы исправить код ( auto возвращаемый тип, например)). Поэтому может быть разумно (даже если неправильно) AcceptsFoo<Foo> видеть уже определенные элементы. (У CRTP возникла бы проблема с тем, что это первая часть определения, поэтому он все равно ничего не смог бы увидеть).

2. @Jarod42 Доступ к члену класса требует, чтобы класс был полным.

3. «итак, если вы попробуете C<T>, пока T не завершено, и повторите попытку, когда T станет полным, и они должны дать разные ответы, вы напрашиваетесь на неприятности». Похоже, что стандарт ищет проблемы, зачем вообще разрешать проверку концепции для неполного типа, если полный тип может дать другой результат? Другими словами: есть ли какой-либо вариант использования, в котором это помогает?

4. @NoSenseEtAl Это не вопрос полного или неполного — есть много вещей, которые могут измениться в отношении типа между одной точкой в программе. Если у вас есть концепция, проверяющая на лайк… foo(t) тогда HasFoo<T> это может быть false в какой-то момент, а затем добавляется перегрузка, и тогда это становится true . Полный / Неполный — это лишь один из примеров этого, и, вероятно, не самый распространенный.

Ответ №2:

[РЕДАКТИРОВАТЬ] Это работает, как и ожидалось, на g 10.3, 11.2 и текущем clang. Некоторые комментарии указывают, что это неопределенное поведение, поэтому остерегайтесь возможных неожиданных изменений для будущих компиляторов.

Оригинальное решение:

Я также только что наткнулся на это, и мне удалось заставить мой CRTP с концепциями работать, позволяя type быть либо неполным, либо полным с желаемыми ограничениями.

Помимо defined IsFoo , я также определяю IsComplete helper (используя sizeof трюк) и, наконец, IsFooIncomplete следующим образом:

 template<class T> 
concept IsFooIncomplete = !IsComplete<T> || IsFoo<T>;
  

Таким образом, я могу гарантировать, что во время обработки Foo она является неполной, и сразу после завершения класса она является полной и соответствует желаемым IsFoo ограничениям.

 #include <concepts>
#include <type_traits>

template<class T>
concept IsFoo = requires(T self)
{
   {
      self.a
      } -> std::same_as<intamp;>;
};

template<class T>
concept IsComplete = requires(T self)
{
   {
      // You can't apply sizeof to an incomplete type
      sizeof(self)
   };
};

template<class T>
concept IsFooIncomplete = !IsComplete<T> || IsFoo<T>;

#if 0
// Will not compile, because Foo currently has incomplete type
template<IsFoo AFoo>
struct AcceptsFoo
{};
#else
// will compile with IsFooIncomplete
template<IsFooIncomplete AFoo> // no need to use 'class AFoo' here...
struct AcceptsFoo
{};
#endif

struct Foo
{
   int a;
   int b;

   // Foo is incomplete here, but that's fine!
   static_assert(!IsComplete<Foo>);
   AcceptsFoo<Foo> obj;
};

// Foo is now complete, and that's also fine!
static_assert(IsFoo<Foo>);
  

Отлично работает на g версии 10.3.0 (с флагом --std=c 20 ), и я надеюсь, что это сработает и на других компиляторах.

[РЕДАКТИРОВАТЬ] как указано в комментариях, это допускает любой неполный тип, но так и задумано. Только внешнее статическое утверждение будет фильтровать полные варианты типов. Спасибо @David Herring за пример в Bar, я написал его здесь для тестирования:https://godbolt.org/z/sqc75qqMv


[EDIT2] теперь это касается варианта CRTP, без какого-либо неопределенного поведения и без обходного пути IsComplete, просто сохраняя тип для проверки после завершения класса.

 // Will not compile, because Foo currently has incomplete type
template<IsFoo AFoo>
struct AcceptsFoo
{};
#else
template<class AFoo> // will not check IsFoo directly here...
struct AcceptsFoo
{
    using IsFooType = AFoo; // will store type on IsFootType for later checks
};
#endif

struct Foo : public AcceptsFoo<Foo> // will check only when complete
{
    int a;
    int b;
};

// Foo is now complete, and that's also fine!
static_assert(IsFoo<Foo::IsFooType>);

struct FooBar : public AcceptsFoo<FooBar> // will fail once it is complete
{
    //int a;
    int b;
};
// will fail here
static_assert(IsFoo<FooBar::IsFooType>);
  

Это также работает на основных компиляторах: https://godbolt.org/z/e1Gc4Kj5n

Комментарии:

1. » сразу после завершения класса он завершен и соответствует желаемым ограничениям IsFoo. «… и если это не так, что тогда? Стандарт требует, чтобы, если концепция выдает результат для типа в какой-то момент в программе, то она всегда выдает этот результат для этого типа. Итак, что произойдет, если тип не соответствует «желаемым ограничениям IsFoo»?

2.Это IFNDR, потому что значение концепции меняется от одной точки использования к другой: обратите внимание, что это struct Bar {AcceptsFoo<Bar> obj;}; работает и в 3 основных реализациях приводит к тому, что AcceptsFoo<Bar> принимается, когда Bar завершено, но это может измениться завтра.

3. @Nicol Bolas Я действительно не вижу здесь проблемы, поскольку IsComplete будет оценен как false для типа Foo, а IsFoo правильно соответствует ограничениям (если я их изменю, компилятор правильно укажет, какое соответствие не удалось). Не могли бы вы привести мне пример, в котором это не сработало бы?

4. Это правда @Davis Herring, struct Bar принят на g 10… Я не знаю почему. Спасибо за совет. godbolt.org/z/Wbe7E1z4E

5. @igormcoelho: Они действительно говорят, что это IsFoo<Bar> есть false , но они помнят, что это IsFooIncomplete<Bar> выполняется, и не проверяют это снова для дальнейшего использования, даже если Bar оно не является ни неполным, ни Foo. Это может легко привести к нарушениям ODR и любому другому виду безумия.