2

Given

  • a makeDocument function creating a temporary resource and handing it over via std::share_ptr,
  • a deref function that encapsulates the application of the default * operator,
  • and a consumer print function,
  • boost::hana::compose

I've noticed that

compose(print, deref, makeDocument)("good");          // OK
compose(print, compose(deref, makeDocument))("bad");  // UB

and I'd like to understand why. Specifically, why does the latter expression result in the temporary std::shared_ptr handed to deref to be destroyed before its pointee is handed to and processed by print? Why doesn't this happen in the former expression?

(CompilerExplorer example.)


I've managed to strip hana::compose down to the minimum I need for my example:

#include <iostream>
#include <memory>
#include <string>
#include <type_traits>
#include <utility>

template <typename F, typename G>
struct compose {
    F f; G g;
    compose(F f, G g) : f{f}, g{g} {}

    template <typename X>
    decltype(auto) operator()(X const& x) const& {
        return f(g(x));
    }
};

struct Document {
    std::string str;
    Document(std::string&& str) : str{std::move(str)} {}
    ~Document() {
        str = "bad";
    };
};

void print(Document const& doc1) {
    std::cout << doc1.str << std::endl;
}

auto deref = [](auto&& x) -> decltype(auto) {
    return *std::forward<decltype(x)>(x);
};

auto makeDocument = [](std::string&& str) {
    return std::make_shared<Document>(std::move(str));
};

const auto good = compose(compose(print, deref), makeDocument);
const auto bad = compose(print, compose(deref, makeDocument));

int main() {
    good("good");
    bad("good");
}

(The question was originally much longer. Look at the history to see where it comes from.)

Enlico
  • 23,259
  • 6
  • 48
  • 102

1 Answers1

2

This seems like an issue of lifetime extension. The relevant quote from the standard is here:

[class.temporary]

6 The temporary object to which the reference is bound or the temporary object that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference if the glvalue to which the reference is bound was obtained through one of the following:

  • a temporary materialization conversion (7.3.4),
  • [...]
  • a class member access (7.6.1.4) using the . operator where the left operand is one of these expressions and the right operand designates a non-static data member of non-reference type,
  • [...]

The exceptions to this lifetime rule are:

  • A temporary object bound to a reference parameter in a function call (7.6.1.2) persists until the completion of the full-expression containing the call.
  • [...]
  • The lifetime of a temporary bound to the returned value in a function return statement (8.7.3) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.

With this, your example can be further boiled down to the following snippet (godbolt):

auto&& good = std::make_shared<Document>("good");
std::cout<<(*good).str<<std::endl;

auto&& bad = *std::make_shared<Document>("good");
std::cout<<bad.str<<std::endl;

In the upper block, good is an rvalue reference to the shared pointer and therefore does extend the lifetime until the call of std::cout (or in your example, for the whole execution of compose(print, deref)).

On the other hand, bad is a reference to a reference returned by the member function operator*, which leads to the immediate destruction of the shared_ptr according to the last bullet point in the quote above.

Note that if there was a member of shared_ptr which could be explicitly bound to a reference (e.g. something like auto&& r = std::make_shared<Document>("good").wrapped_pointer), lifetime extension would apply again. Those things are further explained in this article, where it is explicitly adviced against using such constructs which depend on lifetime extension. In your example, this could be simply cured by returning by-value from deref.

Enlico
  • 23,259
  • 6
  • 48
  • 102
davidhigh
  • 14,652
  • 2
  • 44
  • 75
  • +1, but I've still some difficulty in mapping my code to your snippet. I'll need more time to understand it. – Enlico May 03 '23 at 05:19
  • @Enlico: Yes, same for me, it's tricky. I think the whole point is that due to lifetime-extension, the composition is not associative, because adding in another perfectly-forwarded identity function with `*make_shared("good")` inside spoils the lifetime-extension (as explicitly mentioned in the standard). [Here](https://godbolt.org/z/br7TPTfPK) is another incarnation without the shared_ptr. – davidhigh May 03 '23 at 08:32