Companion code · Chapter 15

Inheritance, Polymorphism, and Composition: Reviewing an Agent's Design

11 files. About this chapter.

Download ch15.zip

ch15/bench.cpp64 lines
// bench.cpp: direct vs virtual calls. Build with -O2, no sanitizers.
#include <chrono>
#include <cstdio>
#include <memory>
#include <random>
#include <vector>

struct Sensor {
    virtual ~Sensor() = default;
    virtual double calib(double x) const { return x * 1.0001; }
    [[gnu::noinline]] double direct(double x) const {
        return x * 1.0001;
    }
    double inlined(double x) const { return x * 1.0001; }
};
struct Temp : Sensor {
    double calib(double x) const override { return x * 0.9999; }
};
struct Hum : Sensor {
    double calib(double x) const override { return x * 1.0002; }
};
struct Wind : Sensor {
    double calib(double x) const override { return x * 0.9998; }
};
using Ptrs = std::vector<std::unique_ptr<Sensor>>;

template <class Call>
void time_it(const char* name, long n, Call call) {
    double best = 1e9, sink = 0;
    for (int rep = 0; rep < 7; ++rep) {          // keep the best of 7
        auto t0 = std::chrono::steady_clock::now();
        for (long i = 0; i < n; ++i) sink += call(i);
        std::chrono::duration<double> d =
            std::chrono::steady_clock::now() - t0;
        if (d.count() < best) best = d.count();
    }
    std::printf("%-20s %5.2f ns/call (%g)\n", name,
                best / n * 1e9, sink);
}

int main() {
    const long n = 10'000'000;
    Ptrs same, mixed;                            // four sensors each
    for (int k = 0; k < 4; ++k) same.push_back(std::make_unique<Temp>());
    mixed.push_back(std::make_unique<Temp>());
    mixed.push_back(std::make_unique<Hum>());
    mixed.push_back(std::make_unique<Wind>());
    mixed.push_back(std::make_unique<Sensor>());
    std::mt19937 rng(42);
    std::vector<unsigned char> pick(n);          // random sensor order
    for (auto& p : pick) p = static_cast<unsigned char>(rng() % 4);
    std::vector<unsigned char> cycle(n);         // 0,1,2,3,0,1,2,3,...
    for (long i = 0; i < n; ++i) cycle[i] = static_cast<unsigned char>(i % 4);
    auto& s = same;
    auto& m = mixed;
    time_it("inlined", n, [&](long i) { return s[pick[i]]->inlined(i); });
    time_it("direct", n, [&](long i) { return s[pick[i]]->direct(i); });
    time_it("virtual, one type", n,
            [&](long i) { return s[pick[i]]->calib(i); });
    time_it("virtual, 4 cycled", n,
            [&](long i) { return m[cycle[i]]->calib(i); });
    time_it("virtual, 4 random", n,
            [&](long i) { return m[pick[i]]->calib(i); });
}
ch15/compose.cpp40 lines
// compose.cpp: calibration as a part, not a parent
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
#include "sensor.hpp"
namespace stn {
struct Calibration {                   // a small value type
    double scale = 1.0;
    double offset = 0.0;
    double apply(double r) const { return r * scale + offset; }
};
struct Channel {                       // has a sensor, has a
    std::unique_ptr<Sensor> sensor;    // calibration
    Calibration cal;
};
class Station {
public:
    void add(std::unique_ptr<Sensor> s, Calibration c = {}) {
        channels_.push_back(Channel{std::move(s), c});
    }
    void recalibrate(std::size_t i, Calibration c) {
        channels_.at(i).cal = c;
    }
    double read(std::size_t i, std::int64_t t_s) {
        Channel& ch = channels_.at(i);
        return ch.cal.apply(ch.sensor->sample(t_s));
    }
private:
    std::vector<Channel> channels_;    // Station owns channels
};
}  // namespace stn

int main() {
    stn::Station st;
    st.add(std::make_unique<stn::TemperatureSensor>(1001));
    std::cout << "before: " << st.read(0, 21600) << '\n';
    st.recalibrate(0, {1.0, -0.4});    // field correction
    std::cout << "after:  " << st.read(0, 21600) << '\n';
}
ch15/dispatch.cpp28 lines
// dispatch.cpp: a hidden function vs. an overridden one
#include <iostream>
#include <string>

class Sensor {
public:
    virtual ~Sensor() = default;
    std::string label() const { return "sensor"; }    // plain
    virtual std::string unit() const { return "?"; }  // virtual
};

class TemperatureSensor : public Sensor {
public:
    std::string label() const { return "temp"; }      // hides
    std::string unit() const override { return "C"; }
};

void describe(const Sensor& s) {
    std::cout << "via Sensor&: " << s.label() << ' ' << s.unit()
              << '\n';
}

int main() {
    TemperatureSensor t;
    std::cout << "direct:      " << t.label() << ' ' << t.unit()
              << '\n';
    describe(t);
}
ch15/err_override.cpp11 lines
// err_override.cpp: a signature mismatch caught by override
#include "sensor.hpp"
namespace stn {
class HumiditySensor : public Sensor {
public:
    using Sensor::Sensor;
    std::string_view unit() const override { return "%RH"; }
    double sample(int t_s) override { return 60.0 + t_s % 30; }
};
}
int main() {}
ch15/novdtor.cpp37 lines
// novdtor.cpp: delete through a base pointer, no virtual
// destructor. Undefined behavior.
#include <cstdint>
#include <iostream>
#include <vector>

namespace stn {

class Sensor {
public:
    explicit Sensor(std::uint32_t id) : id_{id} {}
    ~Sensor() { std::cout << "~Sensor " << id_ << '\n'; }
    virtual double sample(std::int64_t t) = 0;
private:
    std::uint32_t id_;
};

class WindSensor : public Sensor {
public:
    explicit WindSensor(std::uint32_t id)
        : Sensor{id}, gusts_(1440, 0.0) {}   // a day of gusts
    ~WindSensor() { std::cout << "~WindSensor\n"; }
    double sample(std::int64_t t) override {
        gusts_[static_cast<std::size_t>(t % 1440)] = 3.5;
        return 3.5;
    }
private:
    std::vector<double> gusts_;             // 11,520 heap bytes
};

}  // namespace stn

int main() {
    stn::Sensor* s = new stn::WindSensor{1004};
    std::cout << "wind: " << s->sample(60) << " m/s\n";
    delete s;                               // which destructor?
}
ch15/opening.cpp36 lines
// opening.cpp: the agent's first PA5 draft, trimmed (illustrative)
#include <cstdint>
#include <iostream>
#include <memory>
#include <vector>

namespace stn {
class Sensor {
public:
    explicit Sensor(std::uint32_t id) : id_{id} {}
    ~Sensor() = default;        // "unique_ptr cleans up"
    std::uint32_t id() const { return id_; }
    virtual double sample(std::int64_t t_s) = 0;
private:
    std::uint32_t id_;
};
class WindSensor : public Sensor {
public:
    explicit WindSensor(std::uint32_t id)
        : Sensor{id}, gusts_(1440, 0.0) {}
    double sample(std::int64_t t_s) override {
        gusts_[static_cast<std::size_t>(t_s % 1440)] = 3.5;
        return 3.5;
    }
private:
    std::vector<double> gusts_;
};
}  // namespace stn

int main() {
    std::vector<std::unique_ptr<stn::Sensor>> sensors;
    sensors.push_back(std::make_unique<stn::WindSensor>(1004));
    for (auto& s : sensors) {
        std::cout << s->id() << ',' << s->sample(60) << '\n';
    }
}
ch15/predict.cpp29 lines
// predict.cpp: which function runs?
#include <iostream>
#include <string>

class Sensor {
public:
    Sensor() { std::cout << "A " << unit() << '\n'; }
    virtual ~Sensor() = default;
    virtual std::string unit() const { return "?"; }
    std::string kind() const { return "generic"; }
    void report() const {
        std::cout << "D " << kind() << ' ' << unit() << '\n';
    }
};

class PressureSensor : public Sensor {
public:
    std::string unit() const override { return "hPa"; }
    std::string kind() const { return "pressure"; }
};

int main() {
    PressureSensor ps;
    Sensor* p = &ps;
    std::cout << "B " << p->unit() << '\n';
    std::cout << "C " << p->kind() << '\n';
    p->report();
    std::cout << "E " << p->Sensor::unit() << '\n';
}
ch15/proportion.cpp23 lines
// proportion.cpp: what else a station step costs (companion repo only)
#include <chrono>
#include <cmath>
#include <cstdio>
int main() {
    const long n = 2'000'000;
    char buf[64];
    double sink = 0;
    for (int rep = 0; rep < 3; ++rep) {
        auto t0 = std::chrono::steady_clock::now();
        for (long i = 0; i < n; ++i) {
            int len = std::snprintf(buf, sizeof buf, "%u,%ld,%.2f,%d\n", 1001u, i * 60, 21.75 + i * 1e-6, 3);
            sink += buf[len - 2];
        }
        std::chrono::duration<double> d = std::chrono::steady_clock::now() - t0;
        std::printf("snprintf csv line: %.1f ns\n", d.count() / n * 1e9);
        t0 = std::chrono::steady_clock::now();
        for (long i = 0; i < n; ++i) sink += std::sin(i * 1e-3);
        d = std::chrono::steady_clock::now() - t0;
        std::printf("std::sin: %.1f ns\n", d.count() / n * 1e9);
    }
    std::printf("%g\n", sink);
}
ch15/sensor.hpp40 lines
// sensor.hpp: PA5 sensor hierarchy (two of the four kinds)
#pragma once
#include <cmath>
#include <cstdint>
#include <numbers>
#include <string_view>

namespace stn {
class Sensor {                               // abstract base
public:
    explicit Sensor(std::uint32_t id) : id_{id} {}
    virtual ~Sensor() = default;             // virtual dtor
    Sensor(const Sensor&) = delete;          // no copies, so
    Sensor& operator=(const Sensor&) = delete;   // no slicing
    std::uint32_t id() const { return id_; }
    virtual std::string_view unit() const = 0;   // pure virtual
    virtual double sample(std::int64_t t_s) = 0;
private:
    std::uint32_t id_;
};

class TemperatureSensor final : public Sensor {
public:
    using Sensor::Sensor;
    std::string_view unit() const override { return "C"; }
    double sample(std::int64_t t_s) override {   // daily swing
        return 12.0 + 6.0 * std::sin(2.0 * std::numbers::pi
                                     * t_s / 86400.0);
    }
};

class WindSensor final : public Sensor {
public:
    using Sensor::Sensor;
    std::string_view unit() const override { return "m/s"; }
    double sample(std::int64_t t_s) override {
        return 3.0 + static_cast<double>(t_s % 300) / 100.0;
    }
};
}  // namespace stn
ch15/slicing.cpp36 lines
// slicing.cpp: copying a derived object into a base object
#include <cstdint>
#include <iostream>
#include <vector>

class Sensor {                       // concrete, copyable base
public:
    virtual ~Sensor() = default;
    virtual const char* unit() const { return "?"; }
    virtual double sample(std::int64_t) { return 0.0; }
};
class TemperatureSensor : public Sensor {
public:
    explicit TemperatureSensor(double off) : offset_{off} {}
    const char* unit() const override { return "C"; }
    double sample(std::int64_t t) override {
        return 20.0 + t / 600.0 + offset_;
    }
private:
    double offset_;                     // calibration offset
};
void show(const char* w, Sensor& s) {
    std::cout << w << s.sample(600) << ' ' << s.unit() << '\n';
}
void log_by_value(Sensor s) { show("by value:  ", s); }

int main() {
    TemperatureSensor t{-0.25};
    std::vector<Sensor> sensors;
    sensors.push_back(t);             // copies the Sensor part
    show("original:  ", t);
    show("in vector: ", sensors[0]);
    log_by_value(t);                  // copies the Sensor part
    std::cout << "sizes: " << sizeof t << " vs "
              << sizeof(Sensor) << '\n';
}
ch15/station.cpp37 lines
// station.cpp: a Station owns its sensors and steps time
#include <iomanip>
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
#include "sensor.hpp"

namespace stn {

class Station {
public:
    void add(std::unique_ptr<Sensor> s) {
        sensors_.push_back(std::move(s));
    }
    void step(std::int64_t t_s) {        // one simulated minute
        for (const auto& s : sensors_) { // borrow; never copy
            std::cout << s->id() << ',' << t_s << ','
                      << s->sample(t_s) << ",1  (" << s->unit()
                      << ")\n";
        }
    }
    std::size_t size() const { return sensors_.size(); }
private:
    std::vector<std::unique_ptr<Sensor>> sensors_;   // owner
};

}  // namespace stn

int main() {
    stn::Station st;
    st.add(std::make_unique<stn::TemperatureSensor>(1001));
    st.add(std::make_unique<stn::WindSensor>(1004));
    std::cout << std::fixed << std::setprecision(2);
    for (std::int64_t t = 0; t <= 120; t += 60) st.step(t);
    std::cout << st.size() << " sensors\n";
}