0

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

Question

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?

More Detailed Example Code

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 ) );
Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Paul Houx
  • 1,984
  • 1
  • 14
  • 16

2 Answers2

1

Another way would be to add a class MaterialHolder that would have the shared_ptr as a member.

// *** 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;
};

Usage:

MaterialHolder x;
x.create<BasicMaterial>().color(1, 0, 0);

And you could make some variations like:

  • Having a NullMaterial to handle the case where createis not called.
  • Throw an exception if you try to use a material that was not created.
  • Rename MaterialHolder to MaterialFactory and use that class only for creation purpose (and have a function to move out the pointer).
  • Use unique_ptr instead.

Or you could also always write the code by hand:

auto m1 = std::make_shared<BasicMaterial>();

(*m1)
    .color(1, 0, 0)
    .shininess(200)
    ;
Phil1970
  • 2,605
  • 2
  • 14
  • 15
  • Thanks, Phil! The `MaterialHolder` as a concept is not a bad idea. I think if I'd rename that to simply `Material` and rename the abstract base class to `IMaterial` (for interface, which it actually is), then it's certainly an implementation that is easy to use and not confusing in its naming. And like you said, it will be easy to add checks and convenience methods as well. – Paul Houx Oct 20 '18 at 20:43
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.

Paul Houx
  • 1,984
  • 1
  • 14
  • 16
  • 1
    If you do that, you might want to have move constructors for all your derived classes if they have more than a few simple members. – Phil1970 Oct 20 '18 at 12:48
  • @Phil1970 Yes, that almost goes without saying, but thank you for pointing it out. – Paul Houx Nov 11 '18 at 07:56