Skip to content

Instantly share code, notes, and snippets.

@arrieta
Created December 26, 2020 14:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arrieta/f7ffd10a4bd8c024ce67c6bc0188bda7 to your computer and use it in GitHub Desktop.
Save arrieta/f7ffd10a4bd8c024ce67c6bc0188bda7 to your computer and use it in GitHub Desktop.
Coding Conventions for Virtual Functions in C++
// Example that shows some coding conventions regarding the use of virtual
// functions in C++.
// (C) 2020 Nabla Zero Labs
// MIT License
// Contents of "config.hpp" or similar -- a header containing some build
// configuration compile-time constants.
namespace nzl {
namespace config {
static constexpr const auto debug = true; // whether to build in debug mode
} // namespace config
} // namespace nzl
// Contents of "component.hpp" (it would also declare `#pragma once`)
namespace nzl {
// Assume well-defined `Result`, `Time`, and `State` classes declared elsewhere.
using Result = int;
using Time = int;
using State = int;
class Propagator {
public:
// Unless proven otherwise, destructors of pure virtual classes must be public
// and virtual. In rare cases they can be protected and non-virtual. Those are
// the only two options.
virtual ~Propagator() = default; // let the compiler implement it
// Aside from the destructor, the entire public interface is concrete (it
// contains no virtual methods)
Result propagate(Time t, State yi) const noexcept;
protected:
// Only derived classes can construct/initialize a `Propagator`.
Propagator() = default;
private:
// Virtual methods are private (they can only be called by this abstract base
// class).
virtual Result do_propagate(Time t, State yi) const noexcept = 0;
// No member data whatsoever. `Propagator` is just an interface. Member data
// makes sense on some occasions, but not this time.
};
} // namespace nzl
// Contents of "component.cpp" (it would `#include "component.hpp"`)
namespace nzl {
Result Propagator::propagate(Time t, State yi) const noexcept {
// The abstract base class can call the (private) virtual method in a
// controlled fashion. For example: A compile-time constant expression can be
// used to trigger a "debug" behavior.
if constexpr (nzl::config::debug) {
// run pre-conditions code
auto result = this->do_propagate(t, yi);
// run post-conditions code
return result;
} else {
return this->do_propagate(t, yi);
}
}
} // namespace nzl
// Contents of "main.cpp"
// C++ Standard Library
#include <chrono>
#include <iomanip>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// Here, we would `#include <the/component.hpp>`.
// User-defined concrete class. In this case it is `final` because it is not
// intended to be a base class.
class MyPropagator final : public nzl::Propagator {
public:
// In this case, initializing the base class is not necessary, but it's a good
// habit regardless.
MyPropagator(std::string name) noexcept
: nzl::Propagator(), m_name{std::move(name)} {}
// Just a convenience method in the derived class.
const std::string& name() const noexcept { return m_name; }
private:
// Due to our convention, we know at a glance that `do_xxx` is overriding a
// virtual function. We explicitly declare it as an "override.
nzl::Result do_propagate(nzl::Time t, nzl::State yi) const noexcept override {
std::cout << this->name() << "::"
<< "do_propagate(" << t << ", " << yi << ")\n";
return 0;
}
std::string m_name = {}; // Some arbitrary member variable.
};
// A convenience class that can time propagation calls and report some stats.
class Profiler final : public nzl::Propagator {
public:
explicit Profiler(std::unique_ptr<nzl::Propagator> propagator) noexcept
: nzl::Propagator(), m_prop{std::move(propagator)} {}
// Clear all previous stats
void reset() { m_timings.clear(); }
void stats() const noexcept {
std::chrono::nanoseconds total{0u};
auto run_id = 0;
std::cout << std::setw(7) << "run"
<< " " << std::setw(13) << "nanoseconds\n";
for (auto timing : m_timings) {
total += timing;
std::cout << std::setw(7) << (++run_id) << " " << std::setw(12)
<< timing.count() << "\n";
}
std::cout << "average " << std::setw(12) << total.count() / m_timings.size()
<< " nanos / run\n"
<< "total " << std::setw(12) << total.count() << " nanos\n";
}
private:
nzl::Result do_propagate(nzl::Time t, nzl::State yi) const noexcept override {
// error checking ommitted (e.g., m_prop == nullptr).
auto cpu_beg = std::chrono::system_clock::now();
auto result = m_prop->propagate(t, yi);
auto cpu_end = std::chrono::system_clock::now();
m_timings.emplace_back(cpu_end - cpu_beg);
return result;
}
// May not be the best idea to have Profiler own the nzl::Propagator (as it is
// implied by `unique_ptr`). A better idea may be to receive a raw pointer
// owned by someone else, a `shared_ptr` if we always want to extend lifetime,
// or a `weak_ptr` if we don't want to extend lifetime. However, we ALWAYS
// start with the most restrictive ownership semantics (it is easy to "share
// more as we see fit" than it is to "share less" once all the code is
// incredibly convoluted without clear ownership and "everything has a
// reference or pointer to everything else").
std::unique_ptr<nzl::Propagator> m_prop;
// mutable to make `do_propagate` logically `const`.
mutable std::vector<std::chrono::nanoseconds> m_timings;
};
int main() {
// No need for pointer semantics if the concrete class is known a priori.
MyPropagator propagator("P1");
// User never calls implementation details. Only the methods available in the
// Propagator public interface (i.e., `Propagator::propagate`).
auto result = propagator.propagate(1, 2);
std::cout << "result: " << result << "\n";
// Pointer semantics is fine even though it is completely unnecessary in this
// case.
std::vector<std::unique_ptr<nzl::Propagator>> propagators;
for (auto name : {"P2", "P3", "P4"}) {
propagators.emplace_back(std::make_unique<MyPropagator>(name));
}
for (auto&& p : propagators) {
p->propagate(1, 2);
}
// Time some propagations
auto profiler = Profiler(std::make_unique<MyPropagator>("P5"));
profiler.propagate(1, 2);
profiler.propagate(1, 2);
profiler.propagate(1, 2);
profiler.propagate(1, 2);
profiler.propagate(1, 2);
profiler.stats();
// Reset and profile again
profiler.reset();
profiler.propagate(1, 2);
profiler.stats();
}
@arrieta
Copy link
Author

arrieta commented Dec 26, 2020

Sample Run

$ clang++ nzl-coding-conventions-virtual-functions.cpp -std=c++2a -Wall -Wextra -Werror
$ ./a.out
P1::do_propagate(1, 2)
result: 0
P2::do_propagate(1, 2)
P3::do_propagate(1, 2)
P4::do_propagate(1, 2)
P5::do_propagate(1, 2)
P5::do_propagate(1, 2)
P5::do_propagate(1, 2)
P5::do_propagate(1, 2)
P5::do_propagate(1, 2)
    run  nanoseconds
      1         2000
      2         1000
      3         2000
      4         2000
      5         1000
average         1600 nanos / run
total           8000 nanos
P5::do_propagate(1, 2)
    run  nanoseconds
      1         1000
average         1000 nanos / run
total           1000 nanos

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment