C++ design pattern: multiple ways to load file

2019-07-07 02:18发布

问题:

Summary: In search of the standard C++ design pattern for loading different files via constructor

I have a Base class with some functionality that will be used by all derived classes (e.g. Derived_A, Derived_B). The principal difference is that Derived_A and Derived_B override the load function, which is used by the constructor to load a data file (load may also be called explicitly outside the constructor).

I ran into an unexpected problem from this: the load function called by the constructor treats the class as the Base type, but when I use a default constructor and call the load function explicitly, then the virtual function table permits the intended load function to be called.

This smells like a classic problem, but I can't figure out a way to do it (and I was most recently programming in Python, which I believe, due to weak typing, would always call the intended function).

In the same vein, I'd really like Base::load to be pure virtual / abstract (only derived classes will be instantiated); however, that won't compile (I believe, because the compiler sees that the pure virtual function will be called).

Can you help?

Output:

Loading w/ constructor:
Base::load file_A
Base::load file_B Loading w/ function post construction:
Derived_A::load file_A
Derived_B::load file_B

Code:

#include <iostream>
#include <string>

class Base
{
public:
  Base() {}
  Base(std::string x)
  {
    load(x);
  }
  virtual void load(std::string x)
  {
    std::cout << "\tBase::load " << x << std::endl;
  }
};

class Derived_A : public Base
{
public:
  Derived_A() {}
  Derived_A(std::string x): Base(x) {}
  void virtual load(std::string x)
  {
    std::cout << "\tDerived_A::load " << x << std::endl;
  }
};

class Derived_B : public Base
{
public:
  Derived_B() {}
  Derived_B(std::string x): Base(x) {}
  void virtual load(std::string x)
  {
    std::cout << "\tDerived_B::load " << x << std::endl;
  }
};

int main()
{
  // simpler code, but it doesn't behave as I hoped
  std::cout << "Loading w/ constructor:" << std::endl;
  Base*der_a = new Derived_A(std::string("file_A"));
  Base*der_b = new Derived_B(std::string("file_B"));

  // this is what I want to do
  std::cout << "Loading w/ function post construction:" << std::endl;
  der_a = new Derived_A;
  der_a->load( std::string("file_A") );
  der_b = new Derived_B;
  der_b->load( std::string("file_B") );
  return 0;
}

回答1:

The behavior you see is well defined in C++ -- it's just not useful in this scenario because the class is not fully constructed when you call load(std::string) from Base::Base(std::string).

There are two immediate approaches:

A

You could use a container type which calls load (and perhaps holds on to the string as well). This may be more practical if you need to hold on to instances (e.g. they may have specialized error information).

class Loader 
{
public:
    Loader(Base* const p, const std::string& location) : d_base(p) 
    {
        this->d_base->load(location);
    }

private:
    std::unique_ptr<Base>d_base;
private:
    Loader(const Loader&) = delete;
    Loader& operator=(const Loader&) = delete;  
};

In use:

std::cout << "Loading w/ Loader:\n";
Loader l_der_a(new Derived_A, "file_A");
Loader l_der_b(new Derived_B, "file_B");

B

You could also approach it using a helper function:

class Base {
public:
    template<typename T>
    static void Load(const std::string& x) 
    {
         T().load(x);
    }

    Base() 
    {
    }

    Base(std::string x) 
    {
         /* load(x); << see Load(const std::string&) */
    }

    virtual ~Base() 
    {
    }

    virtual void load(std::string x) = 0;
};

In use:

std::cout << "Loading w/ Base::Load<T>():\n";
Derived_A::Load<Derived_A>("file_A");
Derived_B::Load<Derived_B>("file_B");

And then there are several other approaches and variations - it depends on what fits your design best. With C++, you certainly have options.



回答2:

You can look up "Named Constructor Idiom".