Hacker News new | ask | show | jobs
by kazinator 472 days ago
I don't see how concepts can emulate signatures to the full extent that the target object can be manipulated as if it conformed to an abstract base, without any wrapper object being required to handle it.

Without signatures, we have to use some kind of delegating shim which takes the virtual function calls, and calls the real object. It could be a smart pointer.

With signatures, we don't use smart pointers, just "pointer to signature" pointers. However, I suspect those pointers had to be fat! Because, surely, to delegate the signature function calls to the correct functions in the target object class, we need some vtable-like entity. The signatures feature must generate such a vtable-like table for every combination of signature and target class. But target object has no space reserved in it for that table pointer. The obvious solution is a two-word pointer which holds a pointer to the object, and a pointer to the signature dispatch table specific to the signature type and target object's class.

If we can use concepts to do this, with a smart pointer that ends up being two words (e.g. pointer to its own vtable, and a pointer to the target object), we have broken even in that regard.

1 comments

Here is an example then, assuming you mean this kind of abstrations,

    #include <iostream>

    using namespace std;

    template <typename T>
    concept Speaker = requires (T t) {
        t.speak();
    };

    class Duck {
        public:
        void speak() const {
            cout << "quack";
        }
    };

    class Dog {
        public:
        void speak() const {
            cout << "auau";
        }
    };

    class Cat {
        public:
        void speak() const {
            cout << "miau";
        }
    };

    template<Speaker T>
    void speaking_animal(const T&  animal) {
        animal.speak();
        cout << "\n\n";
    }

    template<Speaker... T>
    void speaking_farm(const T&... animals) {
        auto space_adder = [&](auto creature) -> void {
            creature.speak();
            cout << " ";
        };
        (space_adder(animals), ...);
    }


    int main() {
        Duck duck;
        Dog dog;
        Cat cat;

        speaking_animal(duck);
        
        speaking_animal(dog);

        speaking_animal(cat);

        speaking_farm(duck, dog, cat);

    }

Live example, https://godbolt.org/z/vPhf13xEh
Right, but speaking_animal(x) is not dynamic OOP dispatch; it's a template function that gets instantiated for each animal type.

Moreover, everything here can be done without a concept.

This version of the code builds with g++ -std=c++17. We just get worse diagnostics if we try to use something as a Speaker which doesn't conform.

    #include <iostream>

    using namespace std;

    class Duck {
        public:
        void speak() const {
            cout << "quack";
        }
    };

    class Dog {
        public:
        void speak() const {
            cout << "auau";
        }
    };

    class Cat {
        public:
        void speak() const {
            cout << "miau";
        }
    };

    template<typename T>
    void speaking_animal(const T&  animal) {
        animal.speak();
        cout << "\n\n";
    }

    template<typename... T>
    void speaking_farm(const T&... animals) {
        auto space_adder = [&](auto creature) -> void {
            creature.speak();
            cout << " ";
        };
        (space_adder(animals), ...);
    }


    int main() {
        Duck duck;
        Dog dog;
        Cat cat;

        speaking_animal(duck);
        speaking_animal(dog);
        speaking_animal(cat);
        speaking_farm(duck, dog, cat);
    }
I was thinking about more something along these lines. But note the double indirection: we end up passing the smart pointer animal_pointer by reference.

We achieve the "signature thing" though in that we take these animal objects and effectively get them to to conform to the common animal_pointer abstract base without their cooperation.

    #include <iostream>

    using namespace std;

    class Duck {
    public:
        void speak() const { cout << "quack"; }
    };

    class Dog {
    public:
        void speak() const { cout << "auau"; }
    };

    class Cat {
    public:
        void speak() const { cout << "miau"; }
    };

    class animal_pointer {
    public:
        virtual void speak() const = 0;
    };

    template <typename T> class animal_pointer_impl : public animal_pointer {
    private:
        T *obj;
    public:
        animal_pointer_impl(T *o) : obj(o) { }
        virtual void speak() const { obj->speak(); }
    };

    void animal_api(const animal_pointer &p)
    {
        p.speak();
 cout << '\n';
    }

    int main() {
        Duck duck;
        Dog dog;
        Cat cat;

        animal_pointer_impl<Duck> p0(&duck);
        animal_pointer_impl<Dog> p1(&dog);
        animal_pointer_impl<Cat> p2(&cat);

        animal_api(p0);
        animal_api(p1);
        animal_api(p2);
    }
animal_api is a regular function, which represents some external API that we don't get to recompile.
You missed speaking_farm().