0

Some background

Today I saw, in the body of a function kaboom, a local shared_ptr pointing to a *global object and, being captured by reference and its pointee returned by reference by a lambda, i.e. [&local]() -> auto& { return *local; }; this lambda was stored somehow for further use after the function kaboom returned.

As soon as I saw this, I thought the code was invoking undefined behavior. I tried to make a minimal example to support my claim, and came up with the following.

#include <iostream>
#include <memory>
#include <functional>
#include <vector>

struct Resource {
    const int i{3};
};

std::shared_ptr<Resource> global = std::make_unique<Resource>();

auto getSharedStuff() {
    return global;
}

std::vector<std::function<Resource&()>> actions{};

void kaboom() {
    std::shared_ptr<Resource> local = getSharedStuff();
    actions.push_back([&local]() -> auto& { return *local; });
}

int main() {
    kaboom();
    std::cout << global->i << std::endl;
    std::cout << actions[0]().i << std::endl;
}

The simple fact that the 2 couts give 3 and 0 respectively is proof for me that I'm observing UB (3 is correct, and 0 could have been anything, including 3, but I've been lucky that it was not 3, so the UB is well manifest).

Good.

The code I'm curious about

But before getting there, an intermediate version of the repro above was this:

#include <iostream>
#include <memory>
#include <functional>
#include <vector>

struct Resource {
    Resource(Resource const&) = delete;
    Resource(Resource&&) = delete;
    Resource() { std::cout << this << "'s ctor" << std::endl; }
    ~Resource() { std::cout << this << "'s dtor" << std::endl; }
    void operator()() { std::cout << this << "'s operator()" << std::endl; }
};

std::shared_ptr<Resource> global = std::make_unique<Resource>();

auto getSharedStuff() {
    return global;
}

std::vector<std::function<Resource&()>> actions{};

void kaboom() {
    std::shared_ptr<Resource> local = getSharedStuff();
    actions.push_back([&local]() -> auto& { return *local; });
}

int main() {
    kaboom();
    std::cout << "---" << std::endl;
    actions[0]()();
}

which can result in this output

0xc602b0's ctor
---
0x7f0f48f7f4a0's operator()
0xc602b0's dtor

I'm kind of ok with this, as actions[0]()() is dereferencing a destroyed shared_ptr and calling operator() on the screwed up result, so I accept that this can be screwup too.

What's funny, though, is that removing std::cout << "---" << std::endl; makes UB less manifest, as the output becomes something like this:

0xce92b0's ctor
0xce92b0's operator()
0xce92b0's dtor

So my question is: how can a "simple" line as std::cout << "---" << std::endl; affect this way the manifestation of UB?

Enlico
  • 23,259
  • 6
  • 48
  • 102

0 Answers0