如何对来自抽象基础 class 的子 class 模板的成员执行操作?

How to do actions on members of child class templates from an abstact base class?

我知道这个问题被问了很多,但我有一个特定的用例,所以我不认为它是重复的!

我有一个抽象基础class:

template<int N>
class Child;

class Base
{

public:
     // Factory-like generation of children as Base
     static Ptr<Base> New(int baseN)
     {
         if (baseN == 2) return new Child<2>(); 
         else if (baseN == 3) return new Child<3>()
     }

     // Update
     virtual void update() = 0;
};

我正在写 Base 的一些子代作为 class 模板(在 int 上):

template<int N>
class Child
:
    public Base
{
     // Member, N is not the size of matrix, more like the size of a component in matrix
     Matrix<N> m_member;

public:
     // Implement update
     virtual void update();

     // Should call the passed callable on m_member
     virtual void execute(std::function<void(Matrix<N>&)>&);
};

// Force compilation of Child<N>  for some values of N (of interest, including 3) here

// Then,
int baseN = 3;
Ptr<Base> obj = Base::New(baseN); // will get me a Child<3> as a Base object


auto callable = [](Matrix<3>) ->void {};

// Can I access Child<3>::m_member ??
// Can't cast to Child<baseN> (baseN is not constexpr) and don't want to
// But want to do something like:
obj->execute(callable);
// Which forwards 'callable' to the method from concrete type, probably using a cast?

简而言之,我需要从声明的 Base 对象对 m_member 进行某种访问。 最好是一种从 Base 调用 Child<N>::execute 的方法,而不会将 Base 也作为 N 上的模板。

我 tried/thought-of 的东西包括:

    Matrix<N>
  1. 'Type erasure' 通过将它们隐藏在接口后面,但是由于 Matrix<N> 的接口强烈依赖于 N,这样做会使 classes 变得无用(想想:例如 Vector<N>& Matrix<N>::diag())
  2. Base::New可以做些什么来记录它创建的具体类型吗?我对此表示怀疑,因为类型不是对象。

编辑:(顺便说一句,这是 C++11)

所以,我无意中想出了一个办法;但我不太明白为什么下面的作品(还不精通汇编):

您可以在此处查看和检查最小代码示例:https://onlinegdb.com/CiGR1Fq5z

我很困惑的是 Child<0> 和其他 Child<N> 是完全不同的类型;那么我们怎样才能从一个指向另一个类型的指针访问一个人的成员呢?我很可能依赖 UB,甚至担心会有某种堆栈问题!

作为参考,我在此处包含代码以防 link 死亡。

#include <unordered_map>
#include <vector>
#include <functional>
#include <iostream>

using namespace std;

#ifndef MAX_N_VALUE
    #define MAX_N_VALUE 10
#endif // !MAX_N_VALUE

// ------------------ Lib code

// A dummy number class for testing only
template <int N> struct Number { constexpr static int value = N; };

// Objects to register to the database
struct object
{
    // Members
    string name;

    // construction/Destruction
    object(const string& name) : name(name) {}
    virtual ~object(){};
};


// Database of objects
struct DB
: public unordered_map<string, object*>
{
    // See if we can the object of name "name" and type "T" in the DB
    template <class T>
    bool found(const string& name) const
    {
        unordered_map<string,object*>::const_iterator iter = find(name);
        if (iter != end())
        {
            const T* ptr = dynamic_cast<const T*>(iter->second);
            if (ptr) return true;
            cout << name << " found but it's of another type." << endl;
            return false;
        }
        cout << name << " not found." << endl;
        return false;
    }

    // Return a const ref to the object of name "name" and type "T" in the DB
    // if found. Else, fails
    template <class T>
    const T& getObjectRef(const string& name) const
    {
        unordered_map<string,object*>::const_iterator iter = find(name);
        if (iter != end())
        {
            const T* ptr = dynamic_cast<const T*>(iter->second);
            if (ptr) return *ptr;
            cout << name << " found but it's of another type." << endl;
            abort();
        }
        cout << name << " not found." << endl;
        abort();
    }
};


// Forward declare children templates
template<int N>
class Child;

// The interface class
struct Base
{
    // Construction/Destruction
protected:
    static unsigned counter;
    Base(){}
public:
    virtual ~Base() {}

    // Factory-like generation of children as Base
    // THIS New method needs to know how to construct Child<N>
    // so defining it after Child<N>
    static Base* New(int baseN, DB& db);

    // Update
    virtual void update() = 0;
    
    // Call a callable on a child, the callable interface
    // however is independent on N
    virtual void execute(std::function<void(Base&)>& callable)
    {
        callable(*this);
    }
};

unsigned Base::counter = 0;

// The concrete types, which we register to the DB
template<int N>
struct Child
:
    public Base, public object
{
    // members
    vector<Number<N>> member;

    // Construction/Destruction 
    Child() : Base(), object(string("Child") + to_string(N) + ">"), member(N, Number<N>()) {}
    virtual ~Child() {}

    // Test member method (Has to be virtual)
    virtual vector<Number<N>> test() const
    {
        cout << "Calling Child<" << N << ">::test()" << endl;
        return vector<Number<N>>(N, Number<N>());
    }

    // Implement update
    virtual void update()
    {
        cout << "Calling Child<" << N << ">::update()" << endl;
    };
};

// New Base, This can be much more sophisticated
// if static members are leveraged to register constructors
// and invoke them on demand.
Base* Base::New(int baseN, DB& db)
{
    if (baseN == 2)
    {
        Child<2>* c = new Child<2>();
        db.insert({string("Child<")+std::to_string(2)+">", c});
        return c;
    }
    if (baseN == 3)
    {
        Child<3>* c = new Child<3>();
        db.insert({string("Child<")+std::to_string(3)+">", c});
        return c;
    }
    return nullptr;
}

// Finder template for registered children
template<int N>
struct findChild
{
    // Concrete Type we're matching against
    using type = Child<N>;

    // Stop the recursion?
    static bool stop;

    // Compile-time recursion until the correct Child is caught
    // Recursion goes UP in N values
    static void* castToConcrete(const DB& db, Base* system)
    {
        if (N > MAX_N_VALUE) stop = true;
        if (stop) return nullptr;
        if (db.found<type>(string("Child<")+to_string(N)+">"))
        {
            type* ptr = dynamic_cast<type*>(system);
            return static_cast<void*>(ptr);
        }
        // NOTE: This should jump to the next "compiled" child, not just N+1, but meh;
        return findChild<N+1>::castToConcrete(db, system);
    }
};

// Activate recursive behaviour for arbitraty N
template<int N>
bool findChild<N>::stop = false;

// Explicit specialization to stop the Compile-time recursion at a decent child
template<>
struct findChild<MAX_N_VALUE+1>
{
    using type = Child<MAX_N_VALUE+1>;
    static bool stop;
    static void* castToConcrete(const DB& t, const Base* system)
    {
        return nullptr;
    }
};

// Disactivate recursive behaviour for N = 11
bool findChild<MAX_N_VALUE+1>::stop = true;


// ------------------ App code

int main()
{
    // Create objects database
    DB db;

    // --- Part 1: Application writers can't write generic-enough code

    // Select (from compiled children) a new Base object with N = 2
    // and register it to the DB
    Base* b = Base::New(2, db);
    b->update();

    cout << "Access children by explicit dynamic_cast to Child<N>:" << endl;

    // Get to the object through the objects DB.
    // Child destructor should remove the object from DB too, nut meh again
    const auto& oo = db.getObjectRef<Child<2>>("Child<2>");
    cout << oo.test().size() << endl;

    // --- Part 2: Application writers can write generic code if the compile
    // Child<N> for their N

    cout << "If Child<N> is polymorphic, we can access the correct child from findChild<0>:" << endl;

    // Create a lambda that knows about db, which Base applies on itself
    function<void(Base&)> lambda = [&db](Base& base) -> void {
        // Cast and ignore the result
        void* ptr = findChild<0>::castToConcrete(db, &base);

        // Cast back to Child<0>
        findChild<0>::type* c = static_cast<findChild<0>::type*>(ptr);

        // Now access original Child<N> methods and members from Child<0>
        cout << "Method:\n" << c->test().size() << endl;
        cout << "Member:\n" << c->member.size() << endl;
    };

    b->execute(lambda);

    return 0;
}

我使用 GCC 9 编译,选项如下:

-m64 -Wall -Wextra -Wno-unused-parameter -Wold-style-cast -Wnon-virtual-dtor -O0 -fdefault-inline -ftemplate-depth-200

您似乎希望继承不那么相关的组 类...

std::variant (C++17) 可能更合适:

template<int N>
class Child
{
     // Member, N is not the size of matrix, more like the size of a component in matrix
     Matrix<N> m_member;

public:
     void update();

     void execute(std::function<void(Matrix<N>&)> f) { f(m_member); }
};

using Base = std::variant<Child<2>, Child<3>>;

然后:

void foo(Base& obj)
{
    struct Visitor {
        template <std::size_t N>
        void operator()(Child<N>& c) const
        {
            auto callable = [](Matrix<N>) -> void {/*..*/};
            c.execute(callable);
        }
    } visitor;
    std::visit(visitor, obj);
}

要回答您的编辑,而您的可调用对象采用 Base,您可以按如下方式链接 dynamic_cast

template <int N>
void foo_base(Base& b)
{
    if (auto* child = dynamic_cast<Child<N>*>(&b)) {
        // Job with Child<N>
        std::cout << "Method:" << child->test().size() << std::endl;
        std::cout << "Member:" << child->member.size() << std::endl;
    }
}

template <int... Ns>
void foo_dispatch(std::integer_sequence<int, Ns...>, Base& base)
{
    //(foo_base<Ns>(base), ...); // C++17
    const int dummy[] = {0, (foo_base<Ns>(base), 0)...};
    static_cast<void>(dummy); // Avoid warning about unused variable
}

调用类似于:

function<void(Base&)> lambda = [](Base& base) {
    //foo_dispatch(std::integer_sequence<int, 2, 3>(), base);
    foo_dispatch(std::make_integer_sequence<int, MAX_N_VALUE>(), base);
};

Demo

(std::integer_sequence是C++14,但可以在C++11中实现)

Note: Jarod's answer is still a little bit better if you know possible values of N in Child<N> at compile-time and don't want to provide a way to extend them. Plus, of course, if you can use C++17.

这里我依赖于标准定义的“相似类型”:

4.4 Qualification conversions [conv.equal]

... trimmed ...

Two pointer types T1 and T2 are similar if there exists a type T and integer n > 0 such that:

T1 is cv(1,0) pointer to cv(1,1) pointer to ··· cv(1,n−1) pointer to cv(1,n) T

and

T2 is cv(2,0) pointer to cv(2,1) pointer to ··· cv(2,n−1) pointer to (cv2,n) T

where each cv(i,j) is const, volatile, const volatile, or nothing

同段还给出了转换表达式的条件。 简而言之,通过从 Base 继承,所有 Child<N>* 指针类型都与 Base* 相似,因此彼此相似。

现在,我们知道我们可以 static_cast Child<N>Child<0> 没有问题。 但是从 Child<0>* 访问 Child<3> 成员安全吗?

3.10 Lvalues and rvalues [basic.lval]

  1. If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:
  • the dynamic type of the object,

  • ... trimmed ...

  • a type similar (as defined in 4.4) to the dynamic type of the object

你知道了,通过 Child<0>* 访问 Child<3> 的值实际上是定义的行为。

这段代码:

    Base* b = Base::New(2);
    b->update();
    
    Child<2>* c1 = static_cast<Child<2>*>(b);
    c1->update();
    cout << c1->t.sValue << " " << c1->t.rValue << endl;
    
    Child<0>* c2 = static_cast<Child<0>*>(b);
    c2->update();
    cout << c2->t.sValue << " " << c2->t.rValue << endl;

实际会输出(注意静态变量Test<N>::sValue的值):

Calling Child<2>::update()
Calling Child<2>::update()
2 2
Calling Child<2>::update()
0 2

静态成员将始终指向 Child<0>,正因为如此, Jarod 的回答是对这个问题更好的解决方案。

但是如果想允许扩展可能的N个值,这个方案是可以的;您只需要记住将静态变量放在 Base 而不是 Child<N>.

这是一个最小的例子,展示了如何将 lambda 传递给 Base* 而实际上 lambda 将指针转换为 Child<0> 并对其进行操作:

https://onlinegdb.com/TTcMqOmWi