Steve Love
@IAmSteveLove@mastodon.social
@stevelove.bsky.social
https://moderncsharp.blogspot.com/


Affiliate Link: https://mng.bz/0GEp:
Tell don’t ask
Parameterise from above
Some "below the fold" features
Managing non-trivial dependencies
A word about TDD
A design’s language
Turn heating on at certain times every day if needed
Facility to read the current temperature
via 3rd party API
Facility to activate/deactivate heating system
a different 3rd party API
Probably in a loop:
    if start_time < time_now < end_time
        read sensor
        if heating is off
            if temperature < required
                turn on heating
        else
            if temperature > required
                turn off heatingAny number of reasons -
haven’t finalized the hardware
haven’t yet decided on a provider/library
developed elsewhere in the company
3rd party haven’t finished it yet
…
How do I design so that the choice between A and B is less significant?
Separate concerns:
Turning the heating on or off
vs.
Deciding when to turn it on or off
Uncertainty hidden by abstraction
class heating_system
{
public:
    virtual ~heating_system() noexcept = default;
    virtual bool switch_on() = 0;
    virtual bool switch_off() = 0;
    [[nodiscard]] virtual bool current_status() const noexcept = 0;
        // [[nodiscard]] - C++17
};heating_system(const heating_system &) = default;
heating_system(heating_system &&) = default;
heating_system& operator=(heating_system &&) = default;
heating_system& operator=(const heating_system &) = default;or 6…
protected:
    heating_system() = default;Fake it 'til you make it!
class fake_heating : public heating::heating_system
{
public:
    explicit fake_heating(bool status) noexcept
        : active { status } {}
    bool switch_on() override { return active = true; }
    bool switch_off() override { return active = false; }
    [[nodiscard]] bool current_status() const noexcept override
        { return active; }
private:
    bool active;
};class controller
{
public:
    explicit controller(heating::heating_system * other,
                        std::chrono::duration<int> start,
                        std::chrono::duration<int> end);
    void activate() const;
private:
    std::chrono::duration<int> time_on;
    std::chrono::duration<int> time_off;
    heating::heating_system * const heating;
};void controller::activate() const {
    auto now = chrono::system_clock::now();
    auto base = chrono::floor<chrono::days>(now);   // C++17
    if(now > base + time_on && now <= base + time_off)
        heating->switch_on();
    else
        heating->switch_off();
}tell the controller to activate
vs.
ask the controller if the time is right
TEST(ControllerTest, Outline)
{
    auto heater = make_unique<fake_heating>(false);
    controller ctrl { heater.get(),
                      8h+30min, 16h+30min };
    ctrl.activate();
    ASSERT_TRUE(heater->current_status());
}

activate asks for current time
raw pointers make life(time) hard
chrono::duration not really descriptive
using time_of_day = std::chrono::duration<int>;
using wallclock_time = std::chrono::time_point<std::chrono::system_clock>;
class time_window
{
public:
    time_window(time_of_day start, time_of_day end);
    [[nodiscard]] bool in_window(wallclock_time now) const;
private:
    time_of_day start, end;
};Sometimes, asking is the right thing to do
class controller
{
public:
    controller(shared_ptr<heating_system> heater, const time_window & timer);
    void activate(wallclock_time now) const;
private:
    shared_ptr<heating_system> heater;
    time_window timer;
};
auto heater = make_shared<fake_heating>(false);
time_window window { 8h+00min, 18h+00min };
controller ctrl { heater, window };
ctrl.activate(wallclock_time { 10h+11min });
    // or chrono::system_clock::now()
ASSERT_TRUE(heater->current_status());Test for outcome not implementation
controller ctrl { heater, timer };
ctrl.set_required_temp(25);
ctrl.activate(24, wallclock_time { 8h + 15min });
ASSERT_TRUE(heater->current_status());We just pass in the value to controller
Perhaps there’s a better way…
extern "C" int sensor_read()
class thermostat : public heating_system
{
public:
    thermostat(shared_ptr<heating_system> heater,
               int target_temp,
               function<int()> read);
    bool switch_on() override;
    bool switch_off() override;
    [[nodiscard]]
    bool current_status() const noexcept override;
private:
    shared_ptr<heating_system> heater;
    int target_temp;
    function<int()> read_sensor;
};int hardcoded_sensor() { return 22; }TEST(ThermostatTests, Outline)
{
    auto heater = make_shared<fake_heating>(false);
    auto stat = make_shared<thermostat>(
            heater, 25, hardcoded_sensor);
    stat->switch_on();
    ASSERT_TRUE(heater->current_status());
}extern "C" int sensor_read()int hardcoded_sensor() { return 22; }auto heater = make_shared<fake_heating>(false);
auto stat = make_shared<thermostat>(
        heater, 25, hardcoded_sensor);
time_window window { 8h+00min, 18h+00min };
controller ctrl { stat, window };
ctrl.activate(wallclock_time { 10h+11min });
ASSERT_TRUE(heater->current_status());reading the current temperature is encapsulated
⇐
template<std::invocable Function>   // concepts - C++20
class timer_activator : public heating_system
{
public:
    timer_activator(std::shared_ptr<heating_system> heater, 
                    const time_window & timer,
                    Function get_now);
    bool switch_on() override;
    bool switch_off() override;
    [[nodiscard]] bool current_status() const noexcept override;
private:
    std::shared_ptr<heating_system> heater;
    time_window timer;
    Function get_now;
};Replaces controller altogether
TEST(ActivatorTests, Chain)
{
    auto heating = make_shared<fake_heating>(false);
    auto get_now = []() { return wallclock_time {8h + 01min }; };
    auto read_sensor = []() { return 22; };
    time_window window { 8h, 9h };
    // CTAD - C++20
    auto timer = shared_ptr<heating_system>{ new timer_activator{ heating, window, get_now } };
    auto stat = shared_ptr<heating_system>{ new therm_activator{ timer, 25, read_sensor } };
    stat->switch_on();
    ASSERT_TRUE(heating->current_status());
}Each part of the chain calls—and depends on—the next
timer and stat are also testable independently
How would you add functionality to over-ride the heating?
Something like "Extra Hour"
template<std::invocable Function>
class override_activator : public heating_system
{
public:
    override_activator(std::shared_ptr<heating_system> heater, Function get_now);
    bool switch_on() override;
    bool switch_off() override;
    void over_ride(wallclock_time off_time);    // <- type-specific
    [[nodiscard]] bool current_status() const noexcept override;
    // ...
};auto get_now = []() { return wallclock_time { 8h + 01min }; };
auto over =  shared_ptr<override_activator>{ 
    new override_activator{ heating, get_now } };
// ...
// method specific to override_activator
over->over_ride(wallclock_time { 9h+3min + 1h });Fails to compile
override_activator requires template arguments
Sprinkle a little magic template dust…
template<template<class> typename T,
                         typename B,
                         std::invocable C>
auto make_activator(std::shared_ptr<B> h, const C & call)
{
    return std::make_shared<T<C>>(std::move(h), call);
}…to make using it a little easier
auto over = make_activator<override_activator>(heating, get_now);
// method specific to override_activator
over->over_ride(wallclock_time { 9h+3min + 1h });Let uncertainty inform design, not block it
Own your types and give them names
Tell objects what to do rather than ask them how
Pass dependencies as arguments instead of wiring them in
Extensible design is more than OO
… and vice-versa!

Steve Love
@IAmSteveLove@mastodon.social
@stevelove.bsky.social
https://moderncsharp.blogspot.com/
https://www.linkedin.com/in/steve-love-1198994
and even
@IAmSteveLove
Performance
std::function vs. templated lambda
std::functionclass thermostat : public heating_system
{
public:
    thermostat(shared_ptr<heating_system> heater,
               int target_temp,
               function<int()> read);
    bool switch_on() override;
    bool switch_off() override;
    [[nodiscard]]
    bool current_status() const noexcept override;
private:
    shared_ptr<heating_system> heater;
    int target_temp;
    function<int()> read_sensor;
};bool thermostat::switch_on() {
    if(read_sensor() < target_temp)
        return heater->switch_on();
    else
        return heater->switch_off();
}std::invocabletemplate<std::invocable Function>
class therm_activator : public heating_system
{
public:
    therm_activator(std::shared_ptr<heating_system> heater,
                    int target,
                    Function read_temp);
    bool switch_on() override;
    bool switch_off() override;
    [[nodiscard]] bool current_status() const noexcept override;
private:
    std::shared_ptr<heating_system> heater;
    int required;
    Function read_sensor;
};template<std::invocable Function>
bool therm_activator<Function>::switch_on() {
    if(read_sensor() < required)
        return heater->switch_on();
    else
        return heater->switch_off();
}void BM_ThermostatClass(benchmark::State & state)
{
    auto heating = make_shared<fake_heating>(false);
    auto stat = thermostat {
        heating,
        23,
        []() { return 20; } };
    for(auto _ : state)
    {
        for(auto i = 0ll; i != capacity; ++i)
            benchmark::DoNotOptimize(stat.switch_on());
    }
}void BM_ThermostatTemplate(benchmark::State & state)
{
    auto heating = make_shared<fake_heating>(false);
    auto stat = heating::therm_activator{
        heating,
        23,
        []() { return 20; } };
    for(auto _ : state)
    {
        for(auto i = 0ll; i != capacity; ++i)
            benchmark::DoNotOptimize(stat.switch_on());
    }
}capacity = 250LL
g++ (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r1) 14.1.0
2024-06-01T14:18:11+01:00
Running [...]
Run on (20 X 2995 MHz CPU s)
CPU Caches:
  L1 Data 48 KiB (x10)
  L1 Instruction 32 KiB (x10)
  L2 Unified 1280 KiB (x10)
  L3 Unified 24576 KiB (x1)
----------------------------------------------------------------
Benchmark                      Time             CPU   Iterations
----------------------------------------------------------------
BM_ThermostatClass           469 ns          345 ns      2036364
BM_ThermostatTemplate        211 ns          135 ns      4977778capacity = 2500LL
----------------------------------------------------------------
Benchmark                      Time             CPU   Iterations
----------------------------------------------------------------
BM_ThermostatClass          4701 ns         1782 ns       640000
BM_ThermostatTemplate       2027 ns          725 ns      1120000The trouble with RVO…
class my_activator : public heating_system
{
public:
    explicit my_activator(heating_system & other)
        : ref { other } { }
    [[nodiscard]] bool current_status() const override
        { return ref.current_status(); }
    // ...
private:
    heating_system & ref;
};void shared_scope()
{
    fake f{ };
    my_activator h{ f };
    cout << h.current_status();
    // all fine - both f and h go
    // out of scope
}my_activator make_activator(/*...*/)
{
    fake f{ };
    return my_activator{ f };
}
void run_service()
{
    auto activator = make_activator(/*...*/);
    cout << activator.current_status(); // BANG!
}class heating_system
{
public:
    virtual ~heating_system() noexcept = default;
    heating_system() = default;
    heating_system(const heating_system &) = delete;
    heating_system(heating_system &&) = delete;
    heating_system & operator=(const heating_system &) = delete;
    heating_system && operator=(heating_system &&) = delete;
    [[nodiscard]] virtual bool current_status() const = 0;
};