Suppose I'm writing a 3D renderer in C++11, where I create materials and assign them to a model.
I'd like to be able to create materials using the Named Parameter Idiom, like this:
auto material = Material().color( 1, 0, 0 ).shininess( 200 );
I can then create a model like this:
auto model = Model( "path/to/model.obj", material );
I also want to be able to pass the material as an r-value:
auto model = Model( "path/to/model.obj", Material() );
In this simple use case, I can write my Model
class like this:
class Model {
public:
Model() = default;
Model( const fs::path &path, const Material &material )
: mPath( path ), mMaterial( material ) {}
private:
fs::path mPath;
Material mMaterial;
};
I want to make Material
an abstract base class, so that I can create custom implementations for specific kinds of materials. This means that in my Model
I must store a pointer (or even reference) to the material instead. But then I can no longer pass the material as an r-value.
I could choose to use a std::shared_ptr<Material>
member variable instead, but then it becomes much harder to use the Named Parameter Idiom, because how would I construct the material in that case?
Does any of you have some good advice for me?
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 ) );
Actually, I think I have almost answered my own question in the detailed code example. The shared_ptr<Material>
approach works well, apart from its construction. But I could write the following helper function to remedy that:
namespace render {
template<class T>
std::shared_ptr<T> create( const T& inst ) {
return std::make_shared<T>( inst );
}
}
And now I can create a material and model like this:
auto model = Model( "path/to/model.obj", create( BasicMaterial().color( 0, 0, 1 ) );
This is readable, clear and not too wordy.
Of course, I'd still love to hear other ideas or opinions.