Shared Ownership of existing ressource programming pattern

137 views Asked by At

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 invalidates alpha before I call the constructor of alpha*something_else.
  • If I use make_shared, both expressions store shared_ptrs to copies of alpha, 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 to alpha in the function body Foo, and pass this pointer by value to the constructors of 1-alpha and alpha*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 existing Foo 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_ptrs 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.

2

There are 2 answers

4
JHBonarius On

If you pass the shared_ptr bij value, you give the receiver shared ownership of the object. Simple example

#include <memory>

struct Sub{
    std::shared_ptr<int> a,b;
};

#include <cstdio>

int main() {
    auto s = Sub{};
    {
        auto v = std::make_shared<int>(5);
        s = Sub{v,v};
    }
    printf("reference count %ld", s.a.use_count());
}

returns: reference count 2

0
joergbrech On

It turns out that for my usecase I can avoid shared ownership alltogether. Most expression template libraries provide an EVALUTION_EXPRESSION, which represents the evaluation of a function, given some arguments that may be expressions. When evaluating an EVALUATION_EXPRESSION the arguments get evaluated first and their results are passed into the function.

Using this, I can define a linear_combine function and wrap it in an EVALUATION_EXPRESSION, which shall be the sole owner of alpha:

template <typename Scalar, typename Vector>
Vector linear_combine(Scalar alpha, Vector const& x, Vector const& y){
   return (1.-alpha)*x + alpha*y;
}

auto Foo() (
    auto alpha = [...]; // some calculation, result is an expression, e.g. Div<Sub<Value,Value>,Sub<Value,Value>>
    return EVALUTION_EXPRESSION(linear_combine)(std::move(alpha), something, something_else);
)

Here is a Live Code Example using the expression template library boost::yap.