Skip to content

Instantly share code, notes, and snippets.

@TerensTare
Last active January 9, 2026 23:46
Show Gist options
  • Select an option

  • Save TerensTare/09b4f3fde938fb3cab1628e8ab50a9cc to your computer and use it in GitHub Desktop.

Select an option

Save TerensTare/09b4f3fde938fb3cab1628e8ab50a9cc to your computer and use it in GitHub Desktop.
Covariant function parameters in C++
// Dart allows you to specify a "derived type" for parameters when you override virtual functions with the covariant keyword.
// This tells the compiler you know for this class, the parameters will ever be of this type only.
// (eg. when overriding operator==, you know you can only ever compare your type with objects of the same type).
// The compiler needs to add runtime checks for every case this function is used, which is why by default C++ does not support this feature.
// Nevertheless, we can extend our type hierarchy to support covariant parameters in a few steps, with an in-house error system that fits your coding style.
// This example uses C++20, but if you replace concepts with the equivalent type traits it should work with earlier standards.
#include <concepts>
#include <fmt/core.h>
// Step 1. Declare your base type.
struct value
{
virtual ~value() {}
// Step 2. Declare the function(s) you want to have covariant parameters.
// In this case rhs will be covariant.
virtual value const *add(value const *rhs) const = 0;
// just for debugging
virtual void print() const = 0;
// helper to cast down the hierarchy
template <typename T>
inline T const *as() const noexcept
{
static_assert(std::derived_from<T, std::remove_cvref_t<decltype(*this)>>);
return dynamic_cast<T const *>(this);
}
};
// this can be an exception/whatever
struct bad_value final : value
{
// just propagate the error over the calls.
inline value const *add(value const *) const { return this; }
void print() const { fmt::println("bad value"); }
};
// Step 3. Define helper class implementing the covariance bridge + checks.
template <typename Derived>
struct value_helper : value
{
// Step 4. Override the virtual function to call the derived's class function (if matching) or "fail".
inline value const *add(value const *rhs) const final
{
if (auto r = rhs->as<Derived>())
return self().add(*r);
else
return new bad_value{};
}
// CRTP helpers.
inline auto &self()
{
static_assert(std::derived_from<Derived, value_helper<Derived>>);
return *static_cast<Derived *>(this);
}
inline auto const &self() const
{
static_assert(std::derived_from<Derived, value_helper<Derived>>);
return *static_cast<Derived const *>(this);
}
};
// sample type 1: an integer value
struct int_value final : value_helper<int_value>
{
inline explicit int_value(int n) : n{n} {}
// note this is not virtual
// note rhs is a ref now because we know the pointer is valid
inline int_value const *add(int_value const &rhs) const
{
return new int_value{n + rhs.n};
}
void print() const { fmt::println("int({})", n); }
int n;
};
// sample type 2: a floating point value
struct float_value final : value_helper<float_value>
{
inline explicit float_value(float f) : f{f} {}
inline float_value const *add(float_value const &rhs) const
{
return new float_value{f + rhs.f};
}
void print() const { fmt::println("float({})", f); }
float f;
};
int main()
{
value *v1 = new int_value{42};
value *v2 = new int_value{53};
value *v3 = new float_value{1.0f};
value *v4 = new float_value{2.0f};
v1->add(v2)->print(); // ok
v3->add(v4)->print(); // ok
v1->add(v3)->print(); // bad
delete v4;
delete v3;
delete v2;
delete v1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment