命名参数习惯用法和(抽象)基类

Named Parameter Idiom and (abstract) base classes

本文关键字:抽象 基类 惯用法 参数 习惯      更新时间:2023-10-16

>假设我在 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 ) );

另一种方法是添加一个类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)
;

实际上,我想我在详细的代码示例中几乎回答了我自己的问题。除了构造之外,shared_ptr<Material>方法效果很好。但是我可以编写以下帮助程序函数来解决此问题:

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 ) );

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

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