I know that questions on shared ownership on existing ressources have been asked quite a few times before, but somehow I failed to find an answer to my specific problem. Please correct me if I am wrong.
I am working an an expression template library. This is a bit of a toy library, and one thing that I want to explore is the pros and cons of encoding an algorithm using C++ types via expression templates. This could be used e.g. for lazy evaluation and also algorithmic differentiation. For this to work, a user of the library should be able to modify existing chunks of code, so that it uses the expression templates of my library under the hood via operator overloading.
Now I am unsure how to handle ownership of subexpressions. As an example, consider the following fuction:
auto Foo() (
auto alpha = [...]; // some calculation, result is an expression, e.g. Div<Sub<Value,Value>,Sub<Value,Value>>
return (1-alpha)*something + alpha*something_else;
)
This function creates a new expression of type
Add<Mult<Sub<Value,TYPE_OF_ALPHA>,TYPE_OF_SOMETHING>, Mult<TYPE_OF_ALPHA, TYPE_OF_SOMETHING_ELSE>>
It seems clear, that the expressions representing 1-alpha
and alpha*something_else
should take shared ownership of alpha
, since alpha
will go out of scope as we exit Foo
. This tells me, I should use shared_ptr
members to subexpressions in the expressions. - Or is this already a misconception?
My question
How would I write the constructors of my binary operation expressions Sub
and Mult
, such that expressions truely take up shared ownership of the objects/subexpressions/operands passed into the constructor - in such a way, that the changes a user has to make to the function Foo
stays minimal?
- I don't want to move the object into
1-alpha
, since this invalidatesalpha
before I call the constructor ofalpha*something_else
. - If I use
make_shared
, both expressions storeshared_ptr
s to copies ofalpha
, which is not really shared onwership. Evaluating the resulting expression would mean that each copy of alpha gets evaluated, yielding redundant calculations. - I could create a
shared_ptr
toalpha
in the function bodyFoo
, and pass this pointer by value to the constructors of1-alpha
andalpha*something_else
. But this would be a burden on the user of the library. Preferably, the implementation details get nicely hidden behind operator overloads: Ideally, a user would need only minimal changes to their existingFoo
function to use it with the expression template library. Am I asking for too much, here?
Edit 1:
Here is an example, where I use the second option, of creating copies in the constructors (I think...): https://godbolt.org/z/qn4h3q
Edit 2:
I found a solution that works, but I am reluctant to answer my own question because I am not 100% satisfied with my solution.
If we want to take up shared ownership of a ressource that is a custom class, that we can modify (which it is in my case), we can equip the class with a shared_ptr
to a copy of itself. The shared_ptr
is nullptr
as long as nobody takes up ownership of the object.
#include <memory>
#include <iostream>
class Expression
{
public:
Expression(std::string const& n)
: name(n)
{}
~Expression(){
std::cout<<"Destroy "<<name<<std::endl;
}
std::shared_ptr<Expression> shared_copy()
{
return shared_ptr? shared_ptr : shared_ptr = std::make_shared<Expression>(*this);
}
std::string name;
std::shared_ptr<Expression> subexpression;
private:
std::shared_ptr<Expression> shared_ptr = nullptr;
};
int main()
{
// both a and b shall share ownership of the temporary c
Expression a("a");
Expression b("b");
{
Expression c("c");
a.subexpression = c.shared_copy();
b.subexpression = c.shared_copy();
// rename copy of c now shared between a and b
a.subexpression->name = "c shared copy";
}
std::cout<<"a.subexpression->name = "<<a.subexpression->name<<std::endl;
std::cout<<"b.subexpression->name = "<<b.subexpression->name<<std::endl;
std::cout<<"b.subexpression.use_count() = "<<b.subexpression.use_count()<<std::endl;
}
Output:
Destroy c
a.subexpression->name = c shared copy
b.subexpression->name = c shared copy
b.subexpression.use_count() = 2
Destroy b
Destroy a
Destroy c shared copy
Live Code example: https://godbolt.org/z/xP45q6.
Here is an adaptation of the code example given in Edit 1 using actual expression templates: https://godbolt.org/z/86YGfe.
The reason why I am not happy with this is because I am worried I just really killed any performance gain that I could hope to get from ETs. First of all, I have to carry around shared_ptr
s to operands, which is bad enough as it is and second of all, an expression has to carry around an additional shared_ptr
just for the freak case, that it will be used in a temporary/scoped context. ETs are meant to be light-weight.
If you pass the
shared_ptr
bij value, you give the receiver shared ownership of the object. Simple examplereturns:
reference count 2