Идиома именованных параметров и (абстрактные) базовые классы

Предположим, я пишу 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 ) );

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

Другие решения

На самом деле, я думаю, что почти ответил на свой вопрос в подробном примере кода. 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 ) );

Это читабельно, понятно и не слишком многословно.

Конечно, я все еще хотел бы услышать другие идеи или мнения.

0