命名参数习语和(抽象)基础类
Named Parameter Idiom and (abstract) base classes
假设我正在用 C++11 编写 3D 渲染器,我在其中创建 materials 并将它们分配给模型。
我希望能够使用命名参数习惯用法创建 materials,如下所示:
auto material = Material().color( 1, 0, 0 ).shininess( 200 );
然后我可以创建这样的模型:
auto model = Model( "path/to/model.obj", material );
我还希望能够将 material 作为右值传递:
auto model = Model( "path/to/model.obj", Material() );
在这个简单的用例中,我可以这样写 Model
class:
class Model {
public:
Model() = default;
Model( const fs::path &path, const Material &material )
: mPath( path ), mMaterial( material ) {}
private:
fs::path mPath;
Material mMaterial;
};
问题
我想使 Material
成为一个抽象基础 class,这样我就可以为特定类型的 material 创建自定义实现。这意味着在我的 Model
中我必须存储指向 material 的指针(甚至引用)。但是后来我不能再将 material 作为 r 值传递。
我可以选择使用 std::shared_ptr<Material>
成员变量,但是使用命名参数习语变得更加困难,因为在那种情况下我将如何构造 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 ) );
实际上,我想我已经在详细的代码示例中差不多回答了我自己的问题。 shared_ptr<Material>
方法除了其构造之外效果很好。但我可以编写以下辅助函数来解决这个问题:
namespace render {
template<class T>
std::shared_ptr<T> create( const T& inst ) {
return std::make_shared<T>( inst );
}
}
现在我可以创建一个 material 和这样的模型:
auto model = Model( "path/to/model.obj", create( BasicMaterial().color( 0, 0, 1 ) );
这是可读的,清晰的,不会太罗嗦。
当然,我仍然很想听听其他想法或意见。
另一种方法是添加一个 class 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
的情况。
- 如果您尝试使用未创建的 material,则抛出异常。
- 将
MaterialHolder
重命名为 MaterialFactory
并将 class 仅用于创建目的(并具有移出指针的功能)。
- 改用
unique_ptr
。
或者您也可以手写代码:
auto m1 = std::make_shared<BasicMaterial>();
(*m1)
.color(1, 0, 0)
.shininess(200)
;
假设我正在用 C++11 编写 3D 渲染器,我在其中创建 materials 并将它们分配给模型。
我希望能够使用命名参数习惯用法创建 materials,如下所示:
auto material = Material().color( 1, 0, 0 ).shininess( 200 );
然后我可以创建这样的模型:
auto model = Model( "path/to/model.obj", material );
我还希望能够将 material 作为右值传递:
auto model = Model( "path/to/model.obj", Material() );
在这个简单的用例中,我可以这样写 Model
class:
class Model {
public:
Model() = default;
Model( const fs::path &path, const Material &material )
: mPath( path ), mMaterial( material ) {}
private:
fs::path mPath;
Material mMaterial;
};
问题
我想使 Material
成为一个抽象基础 class,这样我就可以为特定类型的 material 创建自定义实现。这意味着在我的 Model
中我必须存储指向 material 的指针(甚至引用)。但是后来我不能再将 material 作为 r 值传递。
我可以选择使用 std::shared_ptr<Material>
成员变量,但是使用命名参数习语变得更加困难,因为在那种情况下我将如何构造 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 ) );
实际上,我想我已经在详细的代码示例中差不多回答了我自己的问题。 shared_ptr<Material>
方法除了其构造之外效果很好。但我可以编写以下辅助函数来解决这个问题:
namespace render {
template<class T>
std::shared_ptr<T> create( const T& inst ) {
return std::make_shared<T>( inst );
}
}
现在我可以创建一个 material 和这样的模型:
auto model = Model( "path/to/model.obj", create( BasicMaterial().color( 0, 0, 1 ) );
这是可读的,清晰的,不会太罗嗦。
当然,我仍然很想听听其他想法或意见。
另一种方法是添加一个 class 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
的情况。 - 如果您尝试使用未创建的 material,则抛出异常。
- 将
MaterialHolder
重命名为MaterialFactory
并将 class 仅用于创建目的(并具有移出指针的功能)。 - 改用
unique_ptr
。
或者您也可以手写代码:
auto m1 = std::make_shared<BasicMaterial>();
(*m1)
.color(1, 0, 0)
.shininess(200)
;