Skip Navigation
InitialsDiceBearhttps://github.com/dicebear/dicebearhttps://creativecommons.org/publicdomain/zero/1.0/„Initials” (https://github.com/dicebear/dicebear) by „DiceBear”, licensed under „CC0 1.0” (https://creativecommons.org/publicdomain/zero/1.0/)SO
sociablefish @programming.dev

also on lemm.ee with same username

Posts 1
Comments 6

How to implement fully generic double dispatch in C++?

The examples shown below are just toy examples, if you weren't already able to catch on.

In the code snippet below, how should we implement pair_typeid?

```c++ class Base1{ public: constexpr virtual ~Base1() noexcept = default; protected: constexpr Base1() noexcept = default; constexpr Base1(const Base1&) noexcept = default; constexpr Base1(Base1&&) noexcept = default;

constexpr Base1& operator=(const Base1&) noexcept = default; constexpr Base1& operator=(Base1&&) noexcept = default; };

class Base2{ public: constexpr virtual ~Base2() noexcept = default; protected: constexpr Base2() noexcept = default; constexpr Base2(const Base2&) noexcept = default; constexpr Base2(Base2&&) noexcept = default;

constexpr Base2& operator=(const Base2&) noexcept = default; constexpr Base2& operator=(Base2&&) noexcept = default; };

class Derived1_1 final: public Base1{/* code */};

class Derived1_2 final: public Base1{/* code */};

class Derived2_1 final: public Base2{/* code */};

class Derived2_2 final: public Base2{/* code */};

/**

  • @brief Gets the typeid(std::pair<dynamic type of 1st param, dynamic type of 2nd param>).
  • More formally, pair_typeid(r1, r2) == typeid(std::pair<T1, T2>)
  • where T1 is the dynamic type of the referent of r1
  • and T2 is the dynamic type of the referent of r2.
  • @return const std::type_info& */ constexpr const std::type_info& pair_typeid(const Base1&, const Base2&) noexcept; ```

If Derived1_1, Derived1_2, Derived2_1, and Derived2_2 were the only classes to use this function, we could use dynamic_cast or typeid, but that's inelegant so we instead use the visitor pattern in the below impl, because it avoids this. Note how we restrict the extent of the double dispatch.

```c++ class Base1;

class Base2;

class Derived2_1;

class Derived2_2;

namespace detail{ template <class T> constexpr const std::type_info& visit_pair_typeid(const Base1&, const T&) noexcept; }

class Base1{ public: constexpr virtual ~Base1() noexcept = default; protected: constexpr Base1() noexcept = default; constexpr Base1(const Base1&) noexcept = default; constexpr Base1(Base1&&) noexcept = default;

constexpr Base1& operator=(const Base1&) noexcept = default; constexpr Base1& operator=(Base1&&) noexcept = default;

constexpr virtual const std::type_info& visit_pair_typeid(const Derived2_1&) const noexcept = 0; constexpr virtual const std::type_info& visit_pair_typeid(const Derived2_2&) const noexcept = 0;

template <class T> friend constexpr const std::type_info& detail::visit_pair_typeid(const Base1&, const T&) noexcept; };

template <class T> constexpr const std::type_info& detail::visit_pair_typeid(const Base1& star_this, const T& acceptor) noexcept{return star_this.visit_pair_typeid(acceptor);}

/**

  • @brief Gets the typeid(std::pair<dynamic type of 1st param, dynamic type of 2nd param>).
  • More formally, pair_typeid(r1, r2) == typeid(std::pair<T1, T2>)
  • where T1 is the dynamic type of the referent of r1
  • and T2 is the dynamic type of the referent of r2.
  • @return const std::type_info& */ constexpr const std::type_info& pair_typeid(const Base1&, const Base2&) noexcept;

class Base2{ public: constexpr virtual ~Base2() noexcept = default; protected: constexpr Base2() noexcept = default; constexpr Base2(const Base2&) noexcept = default; constexpr Base2(Base2&&) noexcept = default;

constexpr Base2& operator=(const Base2&) noexcept = default; constexpr Base2& operator=(Base2&&) noexcept = default;

constexpr virtual const std::type_info& accept_pair_typeid(const Base1&) const noexcept = 0;

template static constexpr const std::type_info& accept_pair_typeid_impl(const T& star_this, const Base1& visitor) noexcept{return detail::visit_pair_typeid(visitor, star_this);}

friend constexpr const std::type_info& pair_typeid(const Base1& r1, const Base2& r2) noexcept{return r2.accept_pair_typeid(r1);} };

class Derived2_1 final: public Base2{ protected: constexpr const std::type_info& accept_pair_typeid(const Base1& visitor) const noexcept final{return accept_pair_typeid_impl(*this, visitor);} };

class Derived2_2 final: public Base2{ protected: constexpr const std::type_info& accept_pair_typeid(const Base1& visitor) const noexcept final{return accept_pair_typeid_impl(*this, visitor);} };

class Derived1_1 final: public Base1{ protected: constexpr const std::type_info& visit_pair_typeid(const Derived2_1&) const noexcept final{return typeid(std::pair<Derived1_1, Derived2_1>);} constexpr const std::type_info& visit_pair_typeid(const Derived2_2&) const noexcept final{return typeid(std::pair<Derived1_1, Derived2_2>);} };

class Derived1_2 final: public Base1{ protected: constexpr const std::type_info& visit_pair_typeid(const Derived2_1&) const noexcept final{return typeid(std::pair<Derived1_2, Derived2_1>);} constexpr const std::type_info& visit_pair_typeid(const Derived2_2&) const noexcept final{return typeid(std::pair<Derived1_2, Derived2_2>);} }; ```

A minor benefit of the visitor pattern is that adding more visitors is extremely easy. This allows templatization of the visitor.

```c++ class Base1;

class Base2;

class Derived2_1;

class Derived2_2;

namespace detail{ template <class T> constexpr const std::type_info& visit_pair_typeid(const Base1&, const T&) noexcept; }

class Base1{ public: constexpr virtual ~Base1() noexcept = default; protected: constexpr Base1() noexcept = default; constexpr Base1(const Base1&) noexcept = default; constexpr Base1(Base1&&) noexcept = default;

constexpr Base1& operator=(const Base1&) noexcept = default; constexpr Base1& operator=(Base1&&) noexcept = default;

constexpr virtual const std::type_info& visit_pair_typeid(const Derived2_1&) const noexcept = 0; constexpr virtual const std::type_info& visit_pair_typeid(const Derived2_2&) const noexcept = 0;

template <class T> friend constexpr const std::type_info& detail::visit_pair_typeid(const Base1&, const T&) noexcept; };

template <class T> constexpr const std::type_info& detail::visit_pair_typeid(const Base1& star_this, const T& acceptor) noexcept{return star_this.visit_pair_typeid(acceptor);}

/**

  • @brief Gets the typeid(std::pair<dynamic type of 1st param, dynamic type of 2nd param>).
  • More formally, pair_typeid(r1, r2) == typeid(std::pair<T1, T2>)
  • where T1 is the dynamic type of the referent of r1
  • and T2 is the dynamic type of the referent of r2.
  • @return const std::type_info& */ constexpr const std::type_info& pair_typeid(const Base1&, const Base2&) noexcept;

class Base2{ public: constexpr virtual ~Base2() noexcept = default; protected: constexpr Base2() noexcept = default; constexpr Base2(const Base2&) noexcept = default; constexpr Base2(Base2&&) noexcept = default;

constexpr Base2& operator=(const Base2&) noexcept = default; constexpr Base2& operator=(Base2&&) noexcept = default;

constexpr virtual const std::type_info& accept_pair_typeid(const Base1&) const noexcept = 0;

template <class T> static constexpr const std::type_info& accept_pair_typeid_impl(const T& star_this, const Base1& visitor) noexcept{return detail::visit_pair_typeid(visitor, star_this);}

friend constexpr const std::type_info& pair_typeid(const Base1& r1, const Base2& r2) noexcept{return r2.accept_pair_typeid(r1);} };

class Derived2_1 final: public Base2{ protected: constexpr const std::type_info& accept_pair_typeid(const Base1& visitor) const noexcept final{return accept_pair_typeid_impl(*this, visitor);} };

class Derived2_2 final: public Base2{ protected: constexpr const std::type_info& accept_pair_typeid(const Base1& visitor) const noexcept final{return accept_pair_typeid_impl(*this, visitor);} };

template <std::size_t N> class Derived1: public Base1{ protected: constexpr const std::type_info& visit_pair_typeid(const Derived2_1&) const noexcept final{return typeid(std::pair<Derived1, Derived2_1>);} constexpr const std::type_info& visit_pair_typeid(const Derived2_2&) const noexcept final{return typeid(std::pair<Derived1, Derived2_2>);} }; ```

Unfortunately, templatizing the acceptor requires templatizing virtual functions. Workarounds like templatizing base classes don't solve the problem at hand. This problem is a stand-in for a problem where CRTP doesn't work and runtime dispatch is needed, as well as both parameters to the real function being part of an unbounded set of types. What's the real solution to the problem? Which idiom can I use?

1
Pick a side Javascript
  • The default sorter does that, because that way it can sort pretty much anything without breaking at runtime.

    who the fuck decided that not breaking at runtime was more important than making sense?

    this js example of [1, 3, 10].sort() vs [1, 3, 10].sort((a, b) => a - b) will be my go to example of why good defaults are important