Created
December 26, 2020 14:53
-
-
Save arrieta/f7ffd10a4bd8c024ce67c6bc0188bda7 to your computer and use it in GitHub Desktop.
Coding Conventions for Virtual Functions in C++
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample Run