Given
- a
makeDocumentfunction creating a temporary resource and handing it over viastd::share_ptr, - a
dereffunction that encapsulates the application of the default*operator, - and a consumer
printfunction, 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?
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.)
This seems like an issue of lifetime extension. The relevant quote from the standard is here:
With this, your example can be further boiled down to the following snippet (godbolt):
In the upper block,
goodis an rvalue reference to the shared pointer and therefore does extend the lifetime until the call ofstd::cout(or in your example, for the whole execution ofcompose(print, deref)).On the other hand,
badis a reference to a reference returned by the member functionoperator*, which leads to the immediate destruction of theshared_ptraccording 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 fromderef.