Предположим, я пишу 3D-рендерер на C ++ 11, где я создаю материалы и присваиваю их модели.
Я хотел бы иметь возможность создавать материалы с использованием именованного параметра параметров, например:
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 ) );
Это читабельно, понятно и не слишком многословно.
Конечно, я все еще хотел бы услышать другие идеи или мнения.