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";
}