0

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.

joergbrech
  • 2,056
  • 1
  • 5
  • 17
  • yeah, so you rule out the points, which leaved the copy-constructor. Just pass by value. (Why didn't you consider that?) – JHBonarius Dec 15 '20 at 11:32
  • Some way to avoid ownership issue is to split expression template from computation, I mean something like in [CppCon 2015: Joel Falcou PART 3 “Expression Templates: Past, Present, Future”](https://youtu.be/A9trwnv6k-w?t=873) – Jarod42 Dec 15 '20 at 16:25
  • @Jarod42, thanks for the link! I don't really understand what you mean by "split expression template from computation", but maybe I will once I watched the video. From the description it sounds like exactly what I need to be watching right now :) – joergbrech Dec 15 '20 at 16:53
  • Basically, terminal variables are not stored in AST, but just a placeholder. So no issues of life time of terminal variables as there are not present. – Jarod42 Dec 15 '20 at 17:22
  • @Jarod42 Thanks for introducing me to `boost::proto`/`boost::yap`, I think I will consider using yap for my project. But I still don't see how I can solve my problem: Yap doesn't handle this correctly _(by default)_ either: https://godbolt.org/z/85PMT8. The [suggestion in the yap manual](https://www.boost.org/doc/libs/1_74_0/doc/html/boost_yap/manual.html#boost_yap.manual.how_expression_operands_are_treated) tells me I should use `std::move` in such cases, but I am really reluctant to `std::move` from `alpha` twice, as mentioned in my question. How would I implement this using placeholders? – joergbrech Dec 16 '20 at 09:06
  • With placeholder, you return an "(ast) function", You really compose functions, then at the end apply to variable x. Ast expression expression is more complicated as it has to handle ownership of values. – Jarod42 Dec 16 '20 at 09:27

2 Answers2

0

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

JHBonarius
  • 10,824
  • 3
  • 22
  • 41
  • This would be what I meant with _"I could contrieve of some other way of creating a shared_ptr in Foo, and pass that 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."_. But then, maybe there is no way to achieve what I am trying to do and your solution might be the best compromise... – joergbrech Dec 15 '20 at 11:46
  • @joergbrech then you should give a better example of what you are trying to achieve and how you solve it now. "some other way" is kind of broad... Please be as specific as you can. – JHBonarius Dec 15 '20 at 11:48
  • You are right. I will edit this bullet point of the question – joergbrech Dec 15 '20 at 12:02
  • I added a code example, see here: https://godbolt.org/z/qn4h3q – joergbrech Dec 15 '20 at 13:43
0

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.

joergbrech
  • 2,056
  • 1
  • 5
  • 17