【问题标题】:Named Parameter Idiom and (abstract) base classes命名参数习语和(抽象)基类
【发布时间】:2019-03-25 00:47:59
【问题描述】:

假设我正在用 C++11 编写一个 3D 渲染器,我在其中创建材质并将它们分配给模型。

我希望能够使用命名参数成语创建材质,如下所示:

auto material = Material().color( 1, 0, 0 ).shininess( 200 );

然后我可以创建一个这样的模型:

auto model = Model( "path/to/model.obj", material );

我还希望能够将材料作为 r 值传递:

auto model = Model( "path/to/model.obj", Material() );

在这个简单的用例中,我可以这样写我的Model 类:

class Model {
  public:
    Model() = default;
    Model( const fs::path &path, const Material &material )
      : mPath( path ), mMaterial( material ) {}

  private:
    fs::path mPath;
    Material mMaterial;
};

问题

我想让Material 成为一个抽象基类,这样我就可以为特定种类的材料创建自定义实现。这意味着在我的Model 中,我必须存储一个指向该材料的指针(甚至是引用)。但是我不能再将材料作为 r 值传递。

我可以选择使用std::shared_ptr<Material> 成员变量,但是使用命名参数习语变得更加困难,因为在这种情况下我将如何构造材料?

大家有什么好的建议吗?

更详细的示例代码

class Material {
  public:
    virtual ~Material() = default;
    virtual void generateShader() = 0;
    virtual void bindShader() = 0;
};

class BasicMaterial : public Material {
  public:
    BasicMaterial() = default;
    BasicMaterial( const BasicMaterial &rhs ) = default;

    static std::shared_ptr<BasicMaterial> create( const BasicMaterial &material ) {
      return std::make_shared<BasicMaterial>( material );
    }

    void generateShader() override;
    void bindShader() override;

    BasicMaterial& color( float r, float g, float b ) {
      mColor.set( r, g, b );
      return *this;
    }

  private:
    Color mColor;
};

class Model {
  public:
    Model() = default;
    Model( const fs::path &path, ...what to use to pass Material?... )
      : mPath( path ), mMaterial( material ) {}

  private:
    Material  mMaterial; // Error! Can't use abstract base class as a member.
    Material* mMaterial; // We don't own. What if Material is destroyed?
    Material& mMaterial; // Same question. Worse: can't reassign.

    std::shared_ptr<Material> mMaterial; // This? But how to construct?    
};

// Can't say I like this syntax much. Feels wordy and inefficient.
std::shared_ptr<Material> material = 
  BasicMaterial::create( BasicMaterial().color( 1, 0, 0 ) );

auto model = Model( "path/to/model.obj", 
  BasicMaterial::create( BasicMaterial().color( 0, 1, 0 ) );

【问题讨论】:

    标签: c++ c++11 rvalue named-parameters abstract-base-class


    【解决方案1】:

    实际上,我想我已经在详细的代码示例中几乎回答了我自己的问题。 shared_ptr&lt;Material&gt; 方法效果很好,除了它的构造。但我可以编写以下辅助函数来解决这个问题:

    namespace render {
    
    template<class T>
    std::shared_ptr<T> create( const T& inst ) {
      return std::make_shared<T>( inst );
    }
    
    }
    

    现在我可以像这样创建材质和模型:

    auto model = Model( "path/to/model.obj", create( BasicMaterial().color( 0, 0, 1 ) );
    

    这是可读、清晰且不太罗嗦的。

    当然,我还是很想听听其他想法或意见。

    【讨论】:

    • 如果您这样做,您可能希望为所有派生类拥有多个简单成员的移动构造函数。
    • @Phil1970 是的,这几乎是不言而喻的,但感谢您指出。
    【解决方案2】:

    另一种方法是添加一个类MaterialHolder,该类将shared_ptr 作为成员。

    // *** Not tested ***
    class MaterialHolder
    {
    public:
        template <typename T> T &create() 
    { 
            T *data = new T;
            material.reset(data); 
            return *data; 
    }
    
    private:
        std::shared_ptr<Material> material;
    };
    

    用法:

    MaterialHolder x;
    x.create<BasicMaterial>().color(1, 0, 0);
    

    你可以做出一些变化,比如:

    • 有一个NullMaterial 来处理create 未被调用的情况。
    • 如果您尝试使用未创建的材质,则会引发异常。
    • MaterialHolder 重命名为 MaterialFactory 并将该类仅用于创建目的(并具有移出指针的功能)。
    • 请改用unique_ptr

    或者您也可以随时手动编写代码:

    auto m1 = std::make_shared<BasicMaterial>();
    
    (*m1)
        .color(1, 0, 0)
        .shininess(200)
        ;
    

    【讨论】:

    • 谢谢,菲尔! MaterialHolder 作为一个概念并不是一个坏主意。我想如果我将它重命名为简单的Material 并将抽象基类重命名为IMaterial(实际上是接口),那么它肯定是一个易于使用且命名不会混淆的实现。就像你说的,添加检查和便利方法也很容易。
    猜你喜欢
    • 2020-03-18
    • 1970-01-01
    • 2010-12-31
    • 2012-06-22
    • 2012-02-09
    • 2010-09-17
    • 1970-01-01
    • 2023-03-17
    • 2021-07-07
    相关资源
    最近更新 更多