Search code examples
c++memory-management

Why passing a function object as a template argument causes different behavior depending of it being an lvalue/rvalue?


Sorry if the title feels convoluted. Consider the below code, it's not so long (85 lines) and trivial to understand.

#include <stdexcept>
#include <stdio.h>

constexpr void assert_that(bool statement, const char* message) {
    if (!statement) throw std::runtime_error{ message };
}

struct SpeedUpdate {
    double velocity_mps;
};

struct CarDetected {
    double distance_m;
    double velocity_mps;
};

struct BrakeCommand {
    double time_to_collision_s;
};

template <typename T>
struct AutoBrake {
    AutoBrake(const T& publish)
        : collision_threshold_s{ 5 },
        speed_mps{},
        publish{ publish } {}

    void observe(const SpeedUpdate& su) {
        speed_mps = su.velocity_mps;
    }

    void observe(const CarDetected& cd) {
        const auto relative_velocity_mps = speed_mps - cd.velocity_mps;
        const auto time_to_collision_s = cd.distance_m / relative_velocity_mps;
        if (time_to_collision_s > 0 &&
            time_to_collision_s <= collision_threshold_s) {
            publish(BrakeCommand{ time_to_collision_s });
        }
    }

    void set_collision_threshold_s(double x) {
        if (x < 1) throw std::out_of_range{ "Collision less than 1." };
        collision_threshold_s = x;
    }

    double get_collision_threshold_s() const {
        return collision_threshold_s;
    }

    double get_speed_mps() const {
        return speed_mps;
    }
private:
    double collision_threshold_s;
    double speed_mps;
    const T& publish;
};

void alert_when_collision_imminent() {
    int brake_commands_published{};
    // case 1: no segfault
    /*
    auto f = [&brake_commands_published](const BrakeCommand&) { brake_commands_published++; };
    AutoBrake auto_brake{ f };
    */

    // case 2: segfault
    /*
    AutoBrake auto_brake{
        [&brake_commands_published](const BrakeCommand&) {
            brake_commands_published++;
        }
    };
    */

    auto_brake.set_collision_threshold_s(10L);
    auto_brake.observe(SpeedUpdate{ 100L });
    auto_brake.observe(CarDetected{ 100L, 0L });

    assert_that(brake_commands_published == 1, "brake commands published not one");
}

int main() {
    alert_when_collision_imminent();
}

On GCC 13.2.1 in Arch Linux, the program fails when case 2 is uncommented: it causes a segfault when the callback function accesses brake_commands_published.

This doesn't occur in case 1 where, instead of constructing the function object as a rvalue and passing it directly to AutoBrake's constructor, we construct a function object, create an lvalue and then pass that lvalue to the constructor.

I simply don't understand why.

Obs: The code has been extracted from Josh Lospinoso's C++ Crash Course.


Solution

  • AutoBrake's constructor saves a reference to its parameter in a class member.

    In the first case, the reference is to the object f, which remains in scope, it continues to exist, for the duration of the relevant code.

    In the second case, the reference is to a temporary object, that goes out of scope and gets destroyed after the object is constructed. AutoBrake's constructor stores a reference to a temporary object. The temporary object gets destroyed after AutoBrake gets constructed, and it now holds a dangling reference.

    Subsequent usage of the referenced, but destroyed, object becomes undefined behavior.