【问题标题】:How to do actions on members of child class templates from an abstact base class?如何对抽象基类中的子类模板成员执行操作?
【发布时间】:2021-11-14 02:39:19
【问题描述】:

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

我有一个抽象基类:

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 的一些子类作为类模板(在 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&lt;N&gt;::execute 的方法,而无需将Base 也作为N 上的模板。

我尝试过/想过的事情包括:

  1. 通过将Matrix&lt;N&gt; 的“类型擦除”隐藏在接口后面,但是由于Matrix&lt;N&gt; 的接口强烈依赖于N,这样做会使类变得无用(例如:Vector&lt;N&gt;&amp; Matrix&lt;N&gt;::diag()
  2. Base::New 可以做任何事情来记录它创建的具体类型吗?我对此表示怀疑,因为类型不是对象。

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

所以,我不小心想出了一个办法来做到这一点;但我不完全理解为什么以下工作(还不精通组装):

  • 我正在使用对象数据库(unordered_map&lt;string, object*&gt; 其中object 是每个注册对象都必须继承的类)。
  • 创建 Child 后,我们将其注册到名称为 Child&lt;N&gt; 的数据库中。
  • 然后,在应用程序级代码中,有一个findChild&lt;int N&gt; 模板,它使用编译时递归来查找从哪个具体类创建的基指针(在运行时,通过动态转换和测试)。当它找到它时,它可以通过静态方法将它转换为void*findChild&lt;N&gt;::castToConcrete
  • 有趣的是,如果Child&lt;N&gt; 是多态的,我们可以以某种方式使用findChild&lt;0&gt; 来访问有问题的findChild&lt;N&gt;。这迫使我们最多有一个 Child 对象(对于所有可能的 N),我当然可以接受。

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

我很困惑的是Child&lt;0&gt; 和其他Child&lt;N&gt; 是完全不同的类型;那么我们如何从一个指向另一个类型的指针访问一个成员呢?我很可能依赖于 UB,甚至担心会出现某种堆栈问题!

作为参考,我在此处包含代码以防链接失效。

#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

【问题讨论】:

  • 因为您甚至无法联系到execute
  • 是的,因为参数依赖于“Matrix”上的可调用对象
  • 如果您将minimal reproducible example 与错误消息一起发布,问题会更清楚。例如 Ptr 是什么?另外我建议一次解决一个问题。我不知道你想如何通过调用Base::New(baseN); 创建Child&lt;3&gt;,然后我已经迷路了
  • Ptr 只是智能指针类的占位符,我无法构建 MRE,因为 new() 函数依赖于很多其他东西,+ 我什至还没有处于得到错误:)
  • 不管是 lambda 还是任何其他类型的可调用。所以一些客户端代码提供了可调用的。 那个代码怎么知道N是什么?

标签: c++ oop templates multiple-inheritance


【解决方案1】:

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

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实现)

【讨论】:

  • 是的,这看起来是一个可行的解决方案,但会让我失去对子对象的运行时选择。如果没有其他结果,我会认为这是一个例外的答案
  • 你仍然可以拥有Base MakeBase(int baseN) { if (baseN == 2) return {Child&lt;2&gt;()}; else return {Child&lt;3&gt;()}; }
  • 从这个对visit的调用中可以推断出什么Variants参数包?您的访问者应该以某种方式了解Child所有现有实例,但它从哪里获得完整列表?
  • @n.1.8e9-where's-my-sharem.: OP 的工厂也有限,所以我希望有效类型的数量也有限(我的 using Base = std::variant&lt;Child&lt;2&gt;, Child&lt;3&gt;&gt;; 可以扩展,但确实,所有可能的类型都应该在编译时知道)。
  • 当您将Child&lt;2&gt;* 转换为Child&lt;0&gt;* 时,您确实有UB(Demo 具有“无效”输出)(以及您的Db 过于复杂的东西,而您只想链接@987654341 @)。
【解决方案2】:

注意:如果您知道可能的话,Jarod 的回答还是会好一点 Child&lt;N&gt; 在编译时 N 的值,并且不想提供扩展它们的方法。另外,当然,如果您可以使用 C++17。

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

4.4 资格转换 [conv.equal]

...修剪...

如果存在类型 T 和整数 n > 0,则两个指针类型 T1 和 T2 是相似的:

T1 是 cv(1,0) 指向 cv(1,1) 的指针 ··· cv(1,n−1) 指向 cv(1,n) T

T2 是 cv(2,0) 指向 cv(2,1) 的指针··· cv(2,n−1) 指向(cv2,n) T的指针

其中每个 cv(i,j) 是 constvolatileconst volatile,或者什么都没有

同一段还显示了转换表达式的条件。 简而言之,通过继承Base,所有Child&lt;N&gt;*指针类型都与Base*相似,因此彼此相似。

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

3.10 左值和右值 [basic.lval]

  1. 如果程序试图通过非左值的左值访问对象的存储值 以下类型的行为未定义:
  • 对象的动态类型,

  • ...修剪...

  • 与对象的动态类型类似(如 4.4 中定义)的类型

你有它,访问Child&lt;3&gt; 的值虽然Child&lt;0&gt;* 实际上是定义的行为。

这段代码:

    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&lt;N&gt;::sValue的值):

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

静态成员将始终指向Child&lt;0&gt;,因此, Jarod 的回答是解决这个问题的更好方法。

但是如果想要允许扩展可能的 N 值,这个解决方案是可以的;您只需要记住将静态变量放入 Base 而不是 Child&lt;N&gt;

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

https://onlinegdb.com/TTcMqOmWi

【讨论】:

    猜你喜欢
    • 2010-10-04
    • 1970-01-01
    • 2020-08-21
    • 2011-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-01
    相关资源
    最近更新 更多