As the person largely responsible for that original design decision, I can weigh in a bit on the reasons and a bit about where we’ll likely head in the future.
There are two core issues here to my mind: allocation and copy semantics. I would agree with those who’ve said that the performance implications here are more theoretical than practical. This pattern is most heavily used with Cinder’s OpenGL objects, and the cost of allocating any of the underlying GL types easily overwhelms heap vs stack allocation. Relatedly, short-lived GL objects almost always point to a badly performing design, or at least design where performance is not a primary concern.
The other issue here though is centered on copy semantics. In particular, what does a user mean with a statement like myTextureA = myTextureB;
? A GPU-side clone is almost always undesirable - certainly for automatic behavior. An under-the-hood refcount (which an early design of Cinder used) is confusing in practice as users have to question if a given class is using that technique or not. However disallowing copying entirely is problematic when a user wants something perfectly natural like a std::vector<gl::Texture>
. A shared_ptr<>
however provides well-defined copy semantics and again, in practical terms the overhead is easily underwritten by the cost of the underlying GL object. It’s also worth pointing out that atomics overhead is basically only incurred in copying, constructing and destructing. In general we pass *Ref
instances as const&
to avoid this overhead, since it exceeds the double-dereference overhead.
That said, this design antedates widely available rvalue refs, which we likely would have used otherwise. As an aside, a fact I did not fully appreciate before starting Cinder is that design for a library like it has to account for user comfortability with concepts. For better or worse, Cinder is many users’ introduction to C++ itself, so in some instances we are less aggressive with technical complexity than we might be otherwise, though as a general rule we would favor power over simplicity when they’re at odds.
Move semantics allow us to address issues like the std::vector<gl::Texture>
example, while still disambiguating copy semantics (by the blunt instrument of preventing copies outright, at least with GL objects). For other types like ci::Surface
we support stack instantiation as well as the static create()
pattern. ci::Surface
also supports move semantics, and in that case we support copying as there’s well-defined, intuitive behavior.
Going forward, a reasonable first step will be to expose constructors for classes like gl::Texture
, implement move and disallowing copy. However there are subtleties that remain. A simple example would the various methods like ci::gl::draw( const TextureRef &, ... )
. Because we know that gl::Texture
only exist as shared_ptr
, we can write only that variant, but if some users have Texture
and some TextureRef
things are a more complicated. We either need to duplicate all of these methods for a non-Ref variant, or we have to break existing Cinder apps and force passing Texture
by value. There are other cases as well - none insoluble but there’s some nontrivial work to be done.
Hopefully this sheds a little light on how we got to this design - it’s certainly not for lack of consideration, though I understand it’s imperfect. In practice it works well enough, and we’re always looking to improve Cinder, so thanks for weighing in with your question.
-Andrew