CRTP и С++ 1y возврат типа вывода
Недавно я играл с CRTP, когда сталкивался с чем-то, что меня удивляло при использовании с С++ 1y-функциями, тип которых выведен. Работает следующий код:
template<typename Derived>
struct Base
{
auto foo()
{
return static_cast<Derived*>(this)->foo_impl();
}
};
struct Derived:
public Base<Derived>
{
auto foo_impl()
-> int
{
return 0;
}
};
int main()
{
Derived b;
int i = b.foo();
(void)i;
}
Я предположил, что возвращаемый тип из Base<Derived>::foo
был decltype
возвращаемого выражения, но если я изменяю functio foo
следующим образом:
auto foo()
-> decltype(static_cast<Derived*>(this)->foo_impl())
{
return static_cast<Derived*>(this)->foo_impl();
}
Этот код больше не работает, я получаю следующую ошибку (из GCC 4.8.1):
||In instantiation of 'struct Base<Derived>':|
|required from here|
|error: invalid static_cast from type 'Base<Derived>* const' to type 'Derived*'|
||In function 'int main()':|
|error: 'struct Derived' has no member named 'foo'|
Мои вопросы: почему это не работает? Что я мог бы написать, чтобы получить правильный тип возврата, не полагаясь на автоматический вывод типа возврата?
И, ну... вот живой пример.
Ответы
Ответ 1
Почему работает первый пример (вывод типа возврата)?
Определение функции-члена шаблона класса только неявно создается при использовании odr (или явно созданного экземпляра). То есть, исходя из Base<Derived>
, вы не подразумеваете экземпляр тела функции. Следовательно, тип возврата еще не выведен.
В точке (*) инстанцирования Derived
завершается, Derived::foo_impl
объявляется, и вывод типа возврата может быть успешным.
(*) не "the", а "определенные моменты инстанцирования". Есть несколько.
Почему не работает второй пример (trailing-return-type)?
Я предположил, что тип возврата из Base<Derived>::foo
был decltype
возвращаемого выражения, но если я изменяю функцию foo
следующим образом:
Тип trailing-return - часть объявления функции-члена; следовательно, он является частью определения окружающего класса, который требуется создать при выводе из Base<Derived>
. На этом этапе Derived
все еще не завершен, в частности Derived::foo_impl
еще не объявлен.
Что я могу написать, чтобы получить правильный тип возврата без полагаясь на вычет типа автоматического возврата?
Теперь это сложно. Я бы сказал, что это не очень четко определено в стандарте, например. см. этот вопрос.
Вот пример, демонстрирующий, что clang++ 3.4 не находит членов Derived
внутри Base<Derived>
:
template<typename Derived>
struct Base
{
auto foo() -> decltype( std::declval<Derived&>().foo_impl() )
{
return static_cast<Derived*>(this)->foo_impl();
}
};
declval
не требует полного типа, поэтому сообщение об ошибке означает, что нет foo_impl
в Derived
.
Там хак, но я не уверен, совместим ли он:
template<typename Derived>
struct Base
{
template<class C = Derived>
auto foo() -> decltype( static_cast<C*>(this)->foo_impl() )
{
static_assert(std::is_same<C, Derived>{}, "you broke my hack :(");
return static_cast<Derived*>(this)->foo_impl();
}
};
Ответ 2
Я обнаружил решение, возможно, не очень красивое, но я думаю, что он довольно стандартный.
Было указано, что это довольно ограниченно, поскольку предполагает, что foo_impl
может быть реализовано без доступа к другим частям Derived или Base. Спасибо @DyP. Я обновил этот ответ с помощью другого подхода.
В любом случае, с точки зрения ответа на вопрос, почему исходный код не работает, я откладываю все остальные и @Dyp. Я многому научился, хорошо описал.
Основная проблема, в условиях непрофессионала (в моем ограниченном понимании!), заключается в том, что когда компилятор видит эту строку:
struct Derived: public Base<Derived>
он сразу хочет/должен знать некоторую/всю информацию о Base<Derived>
, хотя он еще не видел следующие строки, которые определяют foo_impl
.
Решение состоит в перемещении foo_impl
в другой класс под названием NotQuiteDerived
. Тогда Derived
наследуется от этого, а также от Base<...,...>
. Это позволяет положить foo_impl
до введения Derived
. Тогда нам нужен второй параметр типа шаблона в Base
. Во всяком случае, код может говорить сам за себя!Забастовкa >
Я изменил это на более простой и, возможно, немного лучший подход. Base
не нужно видеть все Derived
, но подпись foo_impl
. Это можно передать вместе с параметром CRTP.
Другой подход теперь более гибкий, чем последний, поскольку он позволяет foo_impl
иметь больший доступ к Derived
и действовать так, как если бы он был эффективным методом Derived
. Мы можем объявить foo_impl
в качестве друга Derived
непосредственно перед struct Derived: ...
. Это позволяет реализовать foo_impl
, чтобы увидеть полное определение всего, и позволяет Base
получить возвращаемый тип foo_impl
.
template<typename Derived, typename TypeOfTheFriendFunction>
struct Base
{
auto foo() -> typename std::function<TypeOfTheFriendFunction> :: result_type
{
return foo_impl_as_friend(static_cast<Derived*>(this) /*, any other args here*/);
}
};
struct Derived;
auto foo_impl_as_friend(Derived * This /*, any other args here*/) -> std::string;
struct Derived:
public Base<Derived, decltype(foo_impl_as_friend ) >
{
private:
void method_that_foo_impl_needs() { } // Just to demonstrate that foo_impl can act as part of Derived
friend decltype(foo_impl_as_friend) foo_impl_as_friend;
};
auto foo_impl_as_friend(Derived *This) -> std::string
{
This -> method_that_foo_impl_needs();
return "a string";
}