CRTP 接口:实现中有不同的 return 类型

CRTP interface: different return types in implementation

注: 在解释和示例中,我使用的是 eigen 库。但是,不熟悉该库的人可能会概括和理解我的问题,例如通过将 ConstColXpr 替换为 std::string_view 并将 Vector 替换为 std::string.

问题: 我想使用 CRTP 创建一个接口,有两个 classes 继承自它,在调用某些成员函数时有以下区别:

两种 return 类型具有相同的维度(例如 2x1 列向量)和相同的接口,即可以以完全相同的方式进行交互。这就是为什么我认为将函数定义为接口的一部分是合理的。 但是,我不知道如何正确地 define/restrict 基 class/interface 中的 return 类型。 auto 可以正常编译和执行,但不会告诉用户任何预期结果。

是否有可能以更清晰的方式定义接口?我尝试使用 std::invoke_result 和实现功能,但我必须包括继承接口之前的类型,这是相当落后的。它并不比 auto 好多少,因为实际类型仍然需要在实现中查找。

一个很好的答案是常见的 Eigen 类型,其中尺寸是明确的。但是,我不希望调用接口函数需要模板参数(我必须使用 Eigen::MatrixBase),因为已经有依赖于接口的代码。 另一个不错的答案是某种允许两种不同 return 类型但不必知道完整派生类型的结构。但欢迎所有答案和其他反馈!

这是说明问题的代码:

#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

好的,我想我找到了一个合理可读的解决方案。仍然欢迎反馈。 我刚刚定义了另一个模板参数,一个 bool,它告诉派生的 class 是否包含数据,并使用 std::conditionalbool 定义了 return 类型:

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

编译和执行正常。有点冗长,但至少可以清楚地表明意图。

其他答案仍然欢迎。

注意:添加另一个答案,因为两个答案都有效且独立。

另一种方法是定义特征class。与上一个答案相比的优势在于,任何想要使用 Base 类型对象的代码都不必具有大量模板参数。多个模板参数有点暗示它们可以混合和匹配,但这里的情况更多是关于一种模板类型在逻辑上暗示应该使用哪些类型。

首先定义一个空的traitsclass:

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 与类型别名一起使用:

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 应该做什么 return - 至少和特征的命名一样清楚 ;)

完整代码如下:

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