简体   繁体   中英

CRTP interface: different return types in implementation

Note: In the explanation and my example, I'm using the eigen library. However, my question can probably be generalised and understood by people not familiar with that library, eg by replacing the ConstColXpr with std::string_view and the Vector with a std::string .

Question: I want to create an interface using CRTP, with two classes inheriting from it which differ in the following way when callling certain member functions:

  • The first class returns a view of a data member (an Eigen::Matrix<...>::ConstColXpr )
  • The second class does not have this data member. Instead, the appropriate values are calculated when the function is called and then returned (as an Eigen::Vector<...> )

Both return types have the same dimensions (eg a 2x1 column vector) and the same interface, ie can be interacted with in the exact same way. That's why I believe it is reasonable to define the function as part of the interface. However, I don't know how to properly define/restrict the return type in the base class/interface . auto compiles and executes fine, but doesn't tell the user anything about what to expect.

Is it possible to define the interface in a clearer way? I tried using std::invoke_result with the implementation function, but then I'd have to include the inheriting types before the interface, which is quite backwards. And it's not much better than auto , as the actual type still has to be looked up in the implementation.

A nice answer would be a common Eigen type, where the dimensions are clear. However, I don't want calls to the interface function to require template parameters (which I'd have to do with Eigen::MatrixBase ), because there's already code depending on the interface. Another nice answer would be some construct that allows two different return types, but without having to know the full derived type. But all answers and also other feedback are welcome!

Here's the code illustrating the issue:

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

On my machine, this prints

d1: 4 5
d2: 4 6

Ok, I think I found a reasonably readable solution. Feedback is still welcome. I just defined another template parameter, a bool , which tells whether the derived class holds data, and defined the return type using std::conditional and that 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;
}

Compiles and executes fine. Bit more verbose, but at least shows the intent clearly.

Other answers are still welcome.

Note: Adding another answer, because both answers are valid, and independent.

A different way is to define a traits class. The advantage over the previous answer is, that any code that wants to use an object of type Base doesn't have to have a multitude of template parameters. Multiple template parameters kind of implies that they can be mixed and matched, but the situation here is more about one template type logically implicating which types are supposed to be used.

First, an empty traits class is defined:

template<typename T>
class BaseTraits {};

This is the full definition, not a forward declaration. Then, it has to be specialised for each type derived from Base :

class Derived1; // forward declaration for the traits class

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

and

class Derived2;

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

Now, Base can use the VectorType with a 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();
};

with the effect that it's now clear what myFunc is supposed to return - at least as clear as the naming of the traits;)

Here's the full code:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM