Возьмем простой пример CRTP:
template <class Target>
struct StaticCallable
{
static auto StaticCall()
{
return Target().Call();
}
using ReturnType = decltype(Target().Call());
};
struct StaticCallableInt : StaticCallable<StaticCallableInt>
{
int Call() { return 2; }
};
int main()
{
StaticCallableInt::ReturnType val;
val = StaticCallableInt::StaticCall();
}
StaticCallable<>
создается с помощью StaticCallableInt
, который на данный момент не является полным типом. Для StaticCall()
это нормально. Вычисление откладывается до тех пор, пока не будет вызвана функция, и к тому времени StaticCallableInt
станет полным типом.
Однако для псевдонима ReturnType
это проблема, поскольку он оценивается на месте и к тому времени его необходимо StaticCallableInt
объявить Call()
. Если эта потребность не будет удовлетворена, этот код не сможет скомпилироваться.
Есть ли хорошие методы для псевдонимов типов в CRTP? Решение, которое мне пришло в голову, — использовать вспомогательный класс для всех необходимых деталей класса:
struct CallableInt
{
int Call() { return 2; }
};
struct StaticCallableInt : StaticCallable<CallableInt> {};
Существуют ли какие-либо другие способы зависимости псевдонимов типов от неполных типов, возможно, путем отсрочки оценки псевдонима?
Вы можете добавить еще один уровень косвенности, поместив псевдоним во внутреннюю структуру.
Это отложит оценку псевдонима:
#include <iostream>
template <class Target>
struct StaticCallable
{
static auto StaticCall()
{
return Target().Call();
}
// Add this struct, with the type alias inside it:
struct RetType
{
using value = decltype(Target().Call());
};
};
struct StaticCallableInt : StaticCallable<StaticCallableInt>
{
int Call() { return 2; }
};
int main()
{
StaticCallableInt::RetType::value val; // use the type alias
val = StaticCallableInt::StaticCall();
std::cout << val;
}
Выход:
2
@JoshuaMaiche отлично, рад помочь!
Вы можете просто создать ReturnType
шаблон псевдонима, как показано ниже:
template <class Target>
struct StaticCallable
{
static auto StaticCall()
{
return Target().Call();
}
//now this is a template
template<typename T = Target>
using RetType = decltype(T().Call());
};
struct StaticCallableInt : StaticCallable<StaticCallableInt>
{
int Call() { return 2; }
};
int main()
{
StaticCallableInt::RetType<> val;
val = StaticCallableInt::StaticCall(); //works now
}
Действительно отличное решение! Я пытался сделать что-то подобное с помощью T = void
и напрямую использовать Target, но это не сработало. Я предполагаю, что использование T делает его зависимым именем, откладывая вызов decltype до тех пор, пока StaticCallableInt не станет полным типом. Кажется, это удобный трюк!
@JoshuaMaiche Да, именно так. Непосредственное использование Target
приведет к прямой замене, а не к различию замены.
Я подумал об этом решении еще немного, и одна вещь, которая меня беспокоила, — это возможность неправильного использования RetType<>
, передав неправильный тип. Одним из решений этой проблемы (которое требует немного большего количества механизмов) является создание структуры, которая принудительно использует зависимое имя, но фактически не использует его: template<typename T, typename> struct PickFirst { using type = T; };
Тогда тип можно последовательно заставить быть Target
, все еще используя зависимое имя. : using RetType = decltype(typename PickFirst<Target, T>::type().Call());
Пример в godbolt: godbolt.org/z/Evoc3PrTM
@JoshuaMaiche Вы также можете использовать SFINAE, например template<typename T = Target, typename = std::enable_if_t<std::is_same_v<T, Target>>> using RetType = decltype(T().Call());
, чтобы другие типы не принимались.
@user12002570 RetType<Bob, void>
избегает вашего SFINAE :p Нужно использовать enable_if_t<cond, bool> = true
или requires
или другую «более сильную» технику SFINAE
@Yakk-AdamNevraumont Да, requires
лучше? если С++ 20 доступен.
Разумно использовать класс-оболочку для задержки оценки. Несмотря на то, что класс-оболочка означает, что
RetVal::type
более многословен, чемRetVal<>
другого решения, мне нравится, что этот подход невозможно использовать неправильно, поэтому он лучше подходит для моего варианта использования.