Задание понятия для типа, который имеет шаблон функции-члена, используя Concepts Lite

Я пытаюсь указать концепцию для ограничения более высокого типа, который имеет шаблон функции-члена с помощью Concepts Lite. Однако я не могу найти в техническую спецификацию или учебник предложение, касающееся шаблонных утверждений внутри концепции.

Как это делается?

Пример: предположим, что у меня есть более высокий тип HKT с шаблоном функции-члена F:

template<class T>
struct HKT {
  template<class U> // this looks like e.g. rebind in std::allocators
  auto F(U) -> HKT<U>;
};

и теперь я хочу указать концепцию ограничения этих более высоких типов типов:

template <template <class> class HKT, class T>
concept HKTWithTemplateMemberFunctionF {
  return requires(HKT<T> h) { // HKT<T> is a type, h is an object
    // HKT<T> needs to have a member function template that 
    // returns HTK<U> where the type U is to be deduced and
    // it can be any type (it is unconstrained)
    template<class U>  // is there a syntax for this?
    h.F(std::declval<U>()) -> HKT<U>; 
  }
}

Заметьте, что я мог бы сделать что-то вроде:

template <template <class> class HKT, class T, class U>
concept HKTWithTemplateMemberFunctionF {
  return requires(HKT<T> h) {
      h.F(std::declval<U>()) -> HKT<U>;
  }
}

но это означает, что мне нужно знать U на узле ограничения.

Мне все равно, если подстановка для данного U терпит неудачу или нет, хотя я могу понять, почему это может быть проблемой: например. примените ограничение, чтобы убедиться, что ваша функция не сбой, а затем сбой приводит к тому, что ограничение было выполнено, но при замещении времени создания экземпляра в шаблоне функции-члена (помогло бы, если бы шаблон функции-члена был ограничен?).

Ответ 1

Подумайте о требованиях, которые вы хотите получить от своего комментария:

// HKT<T> needs to have a member function template that 
// returns HTK<U> where the type U is to be deduced and
// it can be any type (it is unconstrained)

В то время как Concepts требует от нас основывать наши ограничения на конкретные типы, мы можем быть умны в выборе нами конкретных типов, которые мы используем. Что вы подразумеваете под U, это любой тип. Действительно какой-то тип вообще? Подумайте о минимально возможном наборе ограничений, которые у вас есть на U, и постройте тип, который их удовлетворяет. Это называется архетипом U.

Моя первая мысль о "любом типе" на самом деле была бы полурегулярным типом. Тип, который по умолчанию является конструктивным, скопируемым и назначаемым. Все нормальные лакомства:

namespace archetypes {
    // private, only used for concept definitions, never in real code
    struct Semiregular { };
}

archetypes::Semiregular - это конкретный тип, поэтому мы можем использовать его для построения концепции:

template <template <class> class HKT, class T>
concept bool HKTWithTemplateMemberFunctionF = 
  requires(HKT<T> h, archetypes::Semiregular r) {
    {h.F(r)} -> HKT<archetypes::Semiregular>
  };

archetypes::Semiregular является частным типом. Он не должен быть известен HKT, поэтому если h.F(r) корректно сформирован и возвращает тип, конвертируемый в HKT<archetypes::Semiregular>, он почти наверняка является шаблоном функции-члена.

Вопрос в том, хороший ли это архетип? Нужно ли нам U быть полурегулярным, или будут работать нерегулярные типы? Чем меньше операций вам нужно, тем меньше должно присутствовать в вашем архетипе. Возможно, все, что вам нужно, это то, что U является подвижным:

namespace archetypes {
    // private, only used for concept definitions, never in real code
    struct Semiregular { };

    struct Moveable {
        Moveable() = delete;
        Moveable(Moveable&& ) noexcept(false);
        Moveable(Moveable const& ) = delete;
        ~Moveable() = default;

        Moveable& operator=(Moveable const& ) = delete;
        Moveable& operator=(Moveable&& ) noexcept(false);
    };
}

template <template <class> class HKT, class T>
concept bool HKTWithTemplateMemberFunctionF =
  requires(HKT<T> h, archetypes::Moveable m) {
    { h.F(m) } -> HKT<archetypes::Moveable>
  };

Мы тестируем ту же идею - вызов F() с типом, который не является хорошо известным и за исключением возвращаемого типа, чтобы отразить это, следовательно, требуя, чтобы он был шаблоном функции. Но теперь мы даем меньше функциональности типу. Если F() работает над любым, он будет работать на archetypes::Moveable.

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

Ответ 2

Короче говоря, теперь вы (я?) должны предоставить конкретный U:

template <template <class> class HKT, class T, class U = T>
concept HKTWithTemplateMemberFunctionF {
  return requires(HKT<T> h) { // HKT<T> is a type, h is an object
    h.F(std::declval<U>()) -> HKT<U>; 
  }
}

поскольку компилятор не может доказать для всех типов U, которые могут когда-либо существовать, что шаблон-член-функция будет работать, то есть следующее безнадежно:

template <template <class> class HKT, class T>
concept HKTWithTemplateMemberFunctionF {
  return requires(HKT<T> h) {
    template<class U>  // for all those Us that haven't been written yet...
    h.F(std::declval<U>()) -> HKT<U>; 
  }
}

В гипотетической концепции концепт-лимита, разработанной в 5 минут, где мы можем немного ограничить U:

template <template <class> class HKT, class T, 
          InputIterator U = InputIterator()  /* imaginary syntax */ >
concept HKTWithTemplateMemberFunctionF {
  return requires(HKT<T> h) {
      h.F(std::declval<U>()) -> HKT<U>; // Is InputIterator enough to instantiate F?
  }
}

компилятору нужно будет только проверить, достаточно ли модели InputIterator для создания экземпляра h.F, что возможно, даже если h.F не ограничено! Кроме того, U просто проверяет, что он моделирует InputIterator, не нужно даже пытаться проверить h.F на U, так как InputIterator достаточно. Это можно использовать для оптимизации производительности во время компиляции и...

... вероятно, будет взаимодействовать удивительно с SFINAE, поскольку AFAIK вы можете иметь функцию перегрузки (например, для InputIterator), которая принимает все входные итераторы, кроме одного (SFINAE! зачем кому-то это делать?!), и, таким образом, может пройти проверку концепции, но удар по времени создания... грустно.