Интерфейс CRTP: различные типы возврата в реализации - PullRequest
1 голос
/ 15 апреля 2020

Примечание: В объяснении и моем примере я использую библиотеку eigen. Однако мой вопрос, вероятно, может быть обобщен и понят людьми, не знакомыми с этой библиотекой, например, заменив ConstColXpr на std::string_view и Vector на std::string.

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

  • Первый класс возвращает представление члена данных (Eigen::Matrix<...>::ConstColXpr)
  • Второй класс не имеет этого члена данных. Вместо этого соответствующие значения вычисляются, когда функция вызывается и затем возвращается (как Eigen::Vector<...>)

Оба возвращаемых типа имеют одинаковые измерения (например, вектор столбца 2x1) и один и тот же интерфейс, т.е. может взаимодействовать точно так же. Вот почему я считаю разумным определить функцию как часть интерфейса. Тем не менее, Я не знаю, как правильно определить / ограничить тип возвращаемого значения в базовом классе / интерфейсе . auto прекрасно компилируется и выполняется, но ничего не говорит пользователю о том, чего ожидать.

Можно ли определить интерфейс более понятным способом? Я пытался использовать std::invoke_result с функцией реализации, но тогда я должен был бы включить наследуемые типы перед интерфейсом, что довольно наоборот. И это не намного лучше, чем auto, так как фактический тип все еще нужно искать в реализации.

Хороший ответ будет распространенным типом Eigen, где размеры ясны. Однако я не хочу, чтобы вызовы интерфейсной функции требовали параметров шаблона (что я должен был бы делать с Eigen::MatrixBase), потому что уже есть код, зависящий от интерфейса. Другим хорошим ответом была бы некоторая конструкция, которая допускает два разных возвращаемых типа, но без необходимости знать полный производный тип. Но все ответы, а также другие отзывы приветствуются!

Вот код, иллюстрирующий проблему:

#include <Eigen/Dense>
#include <type_traits>
#include <utility>
#include <iostream>

template<typename T>
class Base
{
public:
    auto myFunc(int) const;

protected:
    Base();
};

template<typename T>
Base<T>::Base() {
    /* make sure the function is actually implemented, otherwise generate a
     * useful error message */
    static_assert( std::is_member_function_pointer_v<decltype(&T::myFuncImp)> );
}

template<typename T>
auto Base<T>::myFunc(int i) const {
    return static_cast<const T&>(*this).myFuncImp(i);
}


using Matrix2Xd = Eigen::Matrix<double,2,Eigen::Dynamic>;

class Derived1 : public Base<Derived1>
{
private:
    Matrix2Xd m_data;

public:
    Derived1( Matrix2Xd&& );

private:    
    auto myFuncImp(int) const -> Matrix2Xd::ConstColXpr;
    friend Base;
};

Derived1::Derived1( Matrix2Xd&& data ) :
    m_data {data}
{}

auto Derived1::myFuncImp(int i) const -> Matrix2Xd::ConstColXpr {
    return m_data.col(i);
}


class Derived2 : public Base<Derived2>
{
private:
    auto myFuncImp(int) const -> Eigen::Vector2d;
    friend Base;
};

auto Derived2::myFuncImp(int i) const -> Eigen::Vector2d {
    return Eigen::Vector2d { 2*i, 3*i };
}

int main(){
    Matrix2Xd m (2, 3);
    m <<
        0, 2, 4,
        1, 3, 5;

    Derived1 d1 { std::move(m) };

    std::cout << "d1: " << d1.myFunc(2).transpose() << "\n";

    Derived2 d2;

    std::cout << "d2: " << d2.myFunc(2).transpose() << "\n";

    return 0;
}

На моей машине это печатает

d1: 4 5
d2: 4 6

Ответы [ 2 ]

0 голосов
/ 22 апреля 2020

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

Другим способом является определение класса признаков. Преимущество перед предыдущим ответом состоит в том, что любой код, который хочет использовать объект типа Base, не должен иметь множество параметров шаблона. Несколько параметров шаблона в некотором роде подразумевают, что они могут быть смешаны и сопоставлены, но ситуация здесь больше связана с одним типом шаблона, логически подразумевающим, какие типы предполагается использовать.

Во-первых, определяется пустой класс признаков:

template<typename T>
class BaseTraits {};

Это полное определение, а не предварительная декларация. Затем он должен быть специализированным для каждого типа, полученного из Base:

class Derived1; // forward declaration for the traits class

template<>
class BaseTraits<Derived1>
{
public:
    using VectorType = Matrix2Xd::ConstColXpr;
};

и

class Derived2;

template<>
class BaseTraits<Derived2>
{
public:
    using VectorType = Eigen::Vector2d;
};

Теперь Base может использовать VectorType с typealias:

template<typename T>
class Base
{
public:
    using VectorType = typename BaseTraits<T>::VectorType;

    auto myFunc(int) const -> VectorType; /* note the speaking return type */

protected:
    Base();
};

с тем, что теперь понятно, что myFunc должен возвращать - по крайней мере, так же ясно, как и наименование черт;)

Вот полный код:

#include <Eigen/Dense>
#include <type_traits>
#include <utility>
#include <iostream>


template<typename T>
class BaseTraits {};

template<typename T>
class Base
{
public:
    using VectorType = typename BaseTraits<T>::VectorType;
    auto myFunc(int) const -> VectorType;

protected:
    Base();
};

template<typename T>
Base<T>::Base() {
    /* make sure the function is actually implemented, otherwise generate a
     * useful error message */
    static_assert( std::is_member_function_pointer_v<decltype(&T::myFuncImp)> );
}

template<typename T>
auto Base<T>::myFunc(int i) const -> VectorType {
    return static_cast<const T&>(*this).myFuncImp(i);
}


using Matrix2Xd = Eigen::Matrix<double,2,Eigen::Dynamic>;

class Derived1;

template<>
class BaseTraits<Derived1>
{
public:
    using VectorType = Matrix2Xd::ConstColXpr;
};

class Derived1 : public Base<Derived1>
{
private:
    Matrix2Xd m_data;

public:
    Derived1( Matrix2Xd&& );

private:    
    auto myFuncImp(int) const -> Matrix2Xd::ConstColXpr;
    friend Base;
};

Derived1::Derived1( Matrix2Xd&& data ) :
    m_data {data}
{}

auto Derived1::myFuncImp(int i) const -> Matrix2Xd::ConstColXpr {
    return m_data.col(i);
}

class Derived2;

template<>
class BaseTraits<Derived2>
{
public:
    using VectorType = Eigen::Vector2d;
};

class Derived2 : public Base<Derived2>
{
private:
    auto myFuncImp(int) const -> Eigen::Vector2d;
    friend Base;
};

auto Derived2::myFuncImp(int i) const -> Eigen::Vector2d {
    return Eigen::Vector2d { 2*i, 3*i };
}

int main(){
    Matrix2Xd m (2, 3);
    m <<
        0, 2, 4,
        1, 3, 5;

    Derived1 d1 { std::move(m) };

    std::cout << "d1: " << d1.myFunc(2).transpose() << "\n";

    Derived2 d2;

    std::cout << "d2: " << d2.myFunc(2).transpose() << "\n";

    return 0;
}
0 голосов
/ 15 апреля 2020

Хорошо, я думаю, что нашел разумно читаемое решение. Обратная связь по-прежнему приветствуется. Я только что определил другой параметр шаблона, bool, который сообщает, содержит ли производный класс данные, и определил тип возвращаемого значения, используя std::conditional и что bool:

#include <Eigen/Dense>
#include <type_traits>
#include <utility>
#include <iostream>


using Matrix2Xd = Eigen::Matrix<double,2,Eigen::Dynamic>;
using Eigen::Vector2d;


template<typename T, bool hasData>
class Base
{
public:
    auto myFunc(int) const ->
        std::conditional_t<hasData, Matrix2Xd::ConstColXpr, Vector2d>;

protected:
    Base();
};

template<typename T, bool hasData>
Base<T, hasData>::Base() {
    static_assert( std::is_member_function_pointer_v<decltype(&T::myFuncImp)> );
}

template<typename T, bool hasData>
auto Base<T, hasData>::myFunc(int i) const ->
std::conditional_t<hasData, Matrix2Xd::ConstColXpr, Vector2d> {
    return static_cast<const T&>(*this).myFuncImp(i);
}



class Derived1 : public Base<Derived1, true>
{
private:
    Matrix2Xd m_data;

public:
    Derived1( Matrix2Xd&& );

private:    
    auto myFuncImp(int) const -> Matrix2Xd::ConstColXpr;
    friend Base;
};

Derived1::Derived1( Matrix2Xd&& data ) :
    m_data {data}
{}

auto Derived1::myFuncImp(int i) const -> Matrix2Xd::ConstColXpr {
    return m_data.col(i);
}


class Derived2 : public Base<Derived2, false>
{
private:
    auto myFuncImp(int) const -> Eigen::Vector2d;
    friend Base;
};

auto Derived2::myFuncImp(int i) const -> Eigen::Vector2d {
    return Eigen::Vector2d { 2*i, 3*i };
}

int main(){
    Matrix2Xd m (2, 3);
    m <<
        0, 2, 4,
        1, 3, 5;

    Derived1 d1 { std::move(m) };

    std::cout << "d1: " << d1.myFunc(2).transpose() << "\n";

    Derived2 d2;

    std::cout << "d2: " << d2.myFunc(2).transpose() << "\n";

    return 0;
}

Компилируется и выполняется нормально. Немного более многословно, но по крайней мере ясно показывает намерение.

Другие ответы все еще приветствуются.

...