1

在存在菱形继承的情况下防止冗余函数调用的好策略是什么?具体来说,假设我们有一个程序:

#include <iostream>

struct A {
    int a;
    A(int a_) : a(a_) {}
    virtual void print() {
        std::cout << "a:  " << a << std::endl;
    }
};

struct B : virtual public A {
    int b;
    B(int a_,int b_) : A(a_), b(b_) {}
    virtual void print() {
        A::print();
        std::cout << "b:  " << b << std::endl;
    }
};

struct C : virtual public A {
    int c;
    C(int a_,int c_) : A(a_), c(c_) {}
    virtual void print() {
        A::print();
        std::cout << "c:  " << c << std::endl;
    }
};

struct D : public B,public C {
    int d;
    D(int a_,int b_,int c_,int d_) : A(a_), B(a_,b_), C(a_,c_), d(d_) {}
    void print() {
        B::print();
        C::print();
        std::cout << "d:  " << d << std::endl;
    }
};

int main() {
    D d(1,2,3,4);
    d.print();
}

当我们调用 d.print() 时,我们得到:

a:  1
b:  2
a:  1
c:  3
d:  4

其中 a 已打印两次。有什么好的方法可以防止这种情况吗?当然,我们可以使用如下代码手动连接连接:

#include <iostream>

struct A {
    int a;
    A(int a_) : a(a_) {}
    virtual void print_() {
        std::cout << "a:  " << a << std::endl;
    }
    virtual void print() {
        A::print_();
    }
};

struct B : virtual public A {
    int b;
    B(int a_,int b_) : A(a_), b(b_) {}
    virtual void print_() {
        std::cout << "b:  " << b << std::endl;
    }
    virtual void print() {
        A::print_();
        B::print_();
    }
};

struct C : virtual public A {
    int c;
    C(int a_,int c_) : A(a_), c(c_) {}
    virtual void print_() {
        std::cout << "c:  " << c << std::endl;
    }
    virtual void print() {
        A::print_();
        C::print_();
    }
};

struct D : public B,public C {
    int d;
    D(int a_,int b_,int c_,int d_) : A(a_), B(a_,b_), C(a_,c_), d(d_) {}
    virtual void print_() {
        std::cout << "d:  " << d << std::endl;
    }
    virtual void print() {
        A::print_();
        B::print_();
        C::print_();
        D::print_();
    }
};

int main() {
    D d(1,2,3,4);
    d.print();
}

正确输出

a:  1
b:  2
c:  3
d:  4

但我想知道是否有更好的方法。就出现这种情况而言,想象一下对象 A、B、C 和 D 的情况很复杂,需要能够将自己写入磁盘。我们只想为每个 A、B、C 和 D 编写一次输出代码,重要的是 D 不要将 A 的信息写两次。

<---编辑--->

这里还有两种解决问题的方法,但它们仍然有点迟钝。第一个来自 Cristian,涉及设置是否已打印 A 的标志

#include <iostream>

struct A {
    int a;
    bool have_printed;
    A(int a_) : have_printed(false), a(a_) {}
    virtual void print() {
        if(have_printed) return;
        std::cout << "a:  " << a << std::endl;
        have_printed=true;
    }
    void clear() {
        have_printed=false;
    }
};

struct B : virtual public A {
    int b;
    B(int a_,int b_) : A(a_), b(b_) {}
    virtual void print() {
        A::print();
        std::cout << "b:  " << b << std::endl;
    }
};

struct C : virtual public A {
    int c;
    C(int a_,int c_) : A(a_), c(c_) {}
    virtual void print() {
        A::print();
        std::cout << "c:  " << c << std::endl;
    }
};

struct D : public B,public C {
    int d;
    D(int a_,int b_,int c_,int d_) : A(a_), B(a_,b_), C(a_,c_), d(d_) {}
    void print() {
        B::print();
        C::print();
        std::cout << "d:  " << d << std::endl;
    }
};

int main() {
    D d(1,2,3,4);
    d.clear();
    d.print();
}

这正确输出。第二种方法更复杂,但可能允许结构增长。基本上,我们将打印机从类中分离出来,然后在每个对象中注册一个打印机列表。当我们想要打印时,我们遍历打印机列表,然后为我们提供正确的输出。我觉得这使用了太多的机器,但我会包括以防其他人有更好的主意:

// A simple unary function.  Technically, the stl version doesn't require
// the operator
template <typename A,typename B>
struct unary {
    virtual B operator () (A a) {};
};

struct A {
    // Data
    int a;

    // A list of pointers to unary functions.  We need pointers to unary
    // functions rather than unary functions since we need the printer
    // to be polymorphic.
    std::list < unary<A*,void>* > printers;
    A(int a_);

    // We actually end up allocating memory for the printers, which is held
    // internally.  Here, we free that memory.
    ~A() {
        for(std::list < unary<A*,void>* >::iterator printer
                =printers.begin();
            printer != printers.end();
            printer++
        )
            delete (*printer);
    }

private:
    // Require for the dynamic cast used later
    virtual void ___dummy() {};
};
// Prints out the data for A
struct A_Printer : public unary<A*,void>{
    void operator () (A* a) {
        std::cout << "a:  " << a->a << std::endl;
    }
};
// Adds the printer for a to the list of printers
A::A(int a_) : a(a_) {
    printers.push_back(new A_Printer()); 
}

// Now that the structure is setup, we just need to define the data for b,
// it's printer, and then register the printer with the rest
struct B : virtual public A {
    int b;
    B(int a_,int b_);
};
struct B_Printer : public unary<A*,void>{
    void operator () (A* b) {
        std::cout << "b:  " << dynamic_cast <B*>(b)->b << std::endl;
    }
};
B::B(int a_,int b_) : A(a_), b(b_) {
    printers.push_back(new B_Printer());
}

// See the discussion for B
struct C : virtual public A {
    int c;
    C(int a_,int c_);
};
struct C_Printer : public unary<A*,void>{
    void operator () (A* c) {
        std::cout << "c:  " << dynamic_cast <C*>(c)->c << std::endl;
    }
};
C::C(int a_,int c_) : A(a_), c(c_) {
    printers.push_back(new C_Printer());
}

// See the discussion for B
struct D : public B,public C {
    int d;
    D(int a_,int b_,int c_,int d_);
};
struct D_Printer : public unary<A*,void>{
    void operator () (A* d) {
        std::cout << "d:  " << dynamic_cast <D*>(d)->d << std::endl;
    }
};
D::D(int a_,int b_,int c_,int d_) : A(a_), B(a_,b_), C(a_,c_), d(d_) {
    printers.push_back(new D_Printer());
}

// This actually prints everything.  Basically, we iterate over the printers
// and call each one in term on the input.
void print(A* a) {
    for(std::list < unary<A*,void>* >::iterator printer
            =a->printers.begin();
        printer != a->printers.end();
        printer++
    )
        (*(*printer))(a);
}

int main() {
    D d(1,2,3,4);
    // This should print 1,2,3,4
    print(&d);
}

<---编辑 2--->

tmpearce 有一个好主意,在组装散列表之前将所有信息累积到散列表中。通过这种方式,可以检查个人信息是否已经创建并防止冗余。我认为当信息可以轻松组合时这是一个好主意。如果不是这种情况,可能会有轻微的变化,它结合了 tmpearce 和 Cristian 的想法。在这里,我们传递一个集合(或哈希表,或其他)来跟踪函数是否已被调用。通过这种方式,我们可以检查是否已经计算了某个函数。它不需要永久状态,因此多次调用应该是安全的:

#include <iostream>
#include <set>

struct A {
    int a;
    A(int a_) : a(a_) {}
    virtual void print_(std::set <std::string>& computed) {
        if(computed.count("A") > 0) return;
        computed.insert("A");
        std::cout << "a:  " << a << std::endl;
    }
    void print() {
        std::set <std::string> computed;
        print_(computed);
    }
};

struct B : virtual public A {
    int b;
    B(int a_,int b_) : A(a_), b(b_) {}
    virtual void print_(std::set <std::string>& computed) {
        A::print_(computed);
        if(computed.count("B") > 0) return;
        computed.insert("B");
        std::cout << "b:  " << b << std::endl;
    }
};

struct C : virtual public A {
    int c;
    C(int a_,int c_) : A(a_), c(c_) {}
    virtual void print_(std::set <std::string>& computed) {
        A::print_(computed);
        if(computed.count("C") > 0) return;
        computed.insert("C");
        std::cout << "c:  " << c << std::endl;
    }
};

struct D : public B,public C {
    int d;
    D(int a_,int b_,int c_,int d_) : A(a_), B(a_,b_), C(a_,c_), d(d_) {}
    virtual void print_(std::set <std::string>& computed) {
        B::print_(computed);
        C::print_(computed);
        if(computed.count("D") > 0) return;
        computed.insert("D");
        std::cout << "d:  " << d << std::endl;
    }
};

int main() {
    D d(1,2,3,4);
    d.print();
}

在任何情况下,我都会将此问题标记为已解决。不过,我总是想听到更多的答案。

4

3 回答 3

0

我会在 A 结构中添加一个私有标志(如果菱形超出一个级别,则添加到 B 和 C)并检查它并将其标记为已遍历。这也有助于更复杂(嵌套)的菱形图案。

像这样:

struct A {
    int a;
    A(int a_) : a(a_) {traversed = false;}
    virtual void print() {
        if (traversed) return;
        std::cout << "a:  " << a << std::endl;
        traversed = true;
    }
private:
    bool traversed;
};
于 2012-06-08T18:01:58.273 回答
0

我的方法会结合你提到的方法。我会让虚拟方法做一些不同的事情:

class A
{
   public:
   virtual void getInfo(std::map<std::string,std::string>& output)
   {
      if(output.count("A") == 0)
      {
         output["A"] = "a: 1";
      }
   }
   void print()
   {
      std::map<std::string,std::string> m;
      getInfo(m); //virtual method (most derived will be called)
      std::map<std::string,std::string>::iterator iter = m.begin();
      for(; iter!=m.end(); ++iter)
      {
         std::cout<<iter->second();
      }
   }
};

class B : public A
{
   virtual void getInfo(std::map<std::string,std::string>& output)
   {
      A::getInfo(output);
      if(output.count("B") == 0)
      {
         output["B"] = "b: 2";
      }
   }
};

print现在是一种非虚拟方法,用于getInfo填充容器,然后对其进行迭代以显示/保存输出。因此,在将字符串写入容器并将其添加到容器之前,每个类都可以检查以确保容器尚未包含该继承链级别的所需输出。

于 2012-06-08T23:51:35.240 回答
0

只有一个类构造虚拟基(派生最多的,D)所以我会确保只有一个类打印A对象,并且像它的构造一样,首先发生(如果您将对象写入磁盘,这可能很重要。)

您可以向 的构造函数添加一个void*参数A并将其存储在A. 每个派生类都会将虚拟基础构造为A(a, this).

向 中添加一个新成员函数Ado_print(void*)并让每个派生类调用do_print(this)而不是A::print(). 该do_print(void*)函数将其参数与void*传递给Actor 的存储参数进行比较,并且仅在相同时打印。这依赖于每个派生类都有一个不同的地址,如果所有类都是非空的,这将是正确的,但如果这个假设成立,它确保只有最派生的对象打印虚拟基。

于 2012-06-20T00:22:04.800 回答