Skip to content
This repository was archived by the owner on Jan 29, 2026. It is now read-only.
This repository was archived by the owner on Jan 29, 2026. It is now read-only.

a new way for dynamic dispatch function #348

@fililili

Description

@fililili

These days, I found Swift support 2 ways to do dynamic dispatch. Here is the example:

protocol Speak {
    func speak()
}

struct Dog: Speak {
    func speak() {
        print("Woof!")
    }
}

struct Cat: Speak {
    func speak() {
        print("Meow!")
    }
}

let animals : [any Speak] = [Dog(), Cat()]
for animal in animals {
    animal.speak()
}

func speakGeneric<T: Speak>(_ animal: T) {
    animal.speak()
}

for animal in animals {
    speakGeneric(animal)
}

One way is original OOP (animal.speak() ), one way is generic function (speakGeneric(animal) ). Looks like these ways are same in this example. However, if we replace protocol Speak to protocol Comparable, we can find the difference, only below generic function way dynamic dispatch can work, the original OOP way dynamic dispatch cannot compile.

let comps : [any Comparable] = [1, 2.0, "hello"]

func selfCompare<T: Comparable>(_ a: T) -> Bool {
    return a < a
}

for c in comps {
    print(selfCompare(c))
}

That's because generic function has external type information. When use generic function, we know when a < a, we compare 2 same type objects, but with original OOP way dynamic dispatch, we don't have such knowledge.
So, generic function way dynamic dispatch is more powerful. Then, can we support that in C++?
Yes, c++ has template function already. What we have to do is make the template function can support dynamic dispatch. That means, we have to make the template function can be called by type-erased object. So, if the template function can support incomplete type, it can support type-erased object, too. Here is an example:

#include <bit>
#include <iostream>
#include <array>
#include <string>

class incomplete;

template <typename T>
struct dispatch_object {
    T* p_obj;
    bool(*comparable)(const T*, const T *);
};
using dyn_dispatch_object = dispatch_object<incomplete>;
template <typename T>
const dyn_dispatch_object erase(const dispatch_object<T> obj) {
    return std::bit_cast<const dyn_dispatch_object>(obj);
}

template <typename T>
constexpr bool selfCompare(dispatch_object<T> obj) {
    return obj.comparable(obj.p_obj, obj.p_obj);
}
auto selfCompareErase = selfCompare<incomplete>;


int main() {
    int x = 1;
    constexpr bool (*int_compare)(const int*, const int*) = [](const int* a, const int* b) {
        std::cout << *a << std::endl;
        std::cout << *b << std::endl;
        return *a < *b; 
    };
    dispatch_object<int> int_obj{&x, int_compare};

    selfCompare(int_obj);
    
    double y = 2.3;
    constexpr bool (*double_compare)(const double*, const double*) = [](const double* a, const double* b) {
        std::cout << *a << std::endl;
        std::cout << *b << std::endl;
        return *a < *b; 
    };
    dispatch_object<double> double_obj{&y, double_compare};
    selfCompare(double_obj);
    
    std::string z = "4.5";
    constexpr bool (*string_compare)(const std::string*, const std::string*) = [](const std::string* a, const std::string* b) {
        std::cout << *a << std::endl;
        std::cout << *b << std::endl;
        return *a < *b; 
    };
    dispatch_object<std::string> string_obj{&z, string_compare};
    selfCompare(string_obj);

    std::array objs{erase(int_obj), erase(double_obj), erase(string_obj)};
    for(const auto& obj : objs) {
        selfCompareErase(obj);
    }
}

The selfCompare is template function, it's full of type information to ensure type safe. So does the selfCompareErase function, it can ensure type safe, too, and it support type-erased object.
It can be even more powerful, we can even write code in this way:

#include <iostream>
#include <array>
#include <cstdint>

class incomplete;

template <typename T>
bool selfCompare2(const std::array<T, 2>* as, std::size_t T_size, bool(*comparable)(const T*, const T *)) {
    std::uintptr_t abegin = reinterpret_cast<std::uintptr_t>(as);
    const T* a0 = reinterpret_cast<const T*>(abegin);
    const T* a1 = reinterpret_cast<const T*>(abegin + T_size);

    return comparable(a0, a1);
}
auto selfCompare2Erase = selfCompare2<incomplete>;

int main() {
    std::array<double, 2> fs{0.4, 0.5};
    bool (*double_compare)(const double*, const double*) = [](const double* a, const double* b) {
        std::cout << *a << std::endl;
        std::cout << *b << std::endl;
        return *a < *b; 
    };

    std::cout << selfCompare2<double>(&fs, sizeof(double), double_compare) << std::endl;    
    std::cout << selfCompare2Erase(
        reinterpret_cast<const std::array<incomplete, 2>*>(&fs),
        sizeof(double),
        reinterpret_cast<bool (*)(const incomplete*, const incomplete*)>(double_compare)
    ) << std::endl;
}

Generic function dynamic dispatch should be the most powerful way dynamic dispatch, I think every template function can be used in this way (if we convert value to ptr or reference by hand).
(In Jave, it's comparble interface is not T->T->bool, but T1->T2->bool, which is hard to use.)
(In rust, it cannot support comparable as dyn trait, for it has object safety limitation.)
(For Swift, it cannot support the final example.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions