1
\$\begingroup\$

Libraries might need to return "context" objects to the caller, and then require the same object in subsequent calls. In some cases, the user is not required to know what's the concrete type of the "context" objects. In these cases, it is beneficial not to require the user to include the declaration of the context object and to erase the type. In C, this can be done through void*.

void* context = lib::produce();
consume(context);

I was looking for modern C++ ways to achieve the same results, and possibly to safely transfer ownership to the caller. I stumbled across the following ways:

  1. shared_ptr< void > -> not type-safe
  2. unique_ptr<void, void(void*) > -> not type-safe
  3. std::any -> only copy-constructable types

So I came up with the following dynamic-polymorphism approach (using C++17):

    // erased.hpp
    struct ErasedType {
        ErasedType() = default;
        virtual ~ErasedType() = default;
    };
    
    // erasable.hpp
    template <typename T>
    struct ErasableType : public ErasedType {
        explicit ErasableType(T data) : ErasedType(), data{std::move(data)} {}
        T data;
    };
    
    template <typename T>
    [[nodiscard]] T& erased_cast(ErasedType& a) {
        ErasableType<T>& typed = dynamic_cast<ErasableType<T>&>(a);
        return typed.data;
    }
    
    template <typename T>
    [[nodiscard]] const T& erased_cast(const ErasedType& a) {
        const ErasableType<T>& typed = dynamic_cast<const ErasableType<T>&>(a);
        return typed.data;
    }

The caller only needs to include erased.hpp and such approach seems to offer a type-safe erasure.

//test.cpp
struct A {
    int value;

    // moveable-only type
    A(const A&) = delete;
    A& operator=(const A&) = delete;

    A(A&&) = default;
    A& operator=(A&&) = default;
};

struct B {};

template <typename Action>
void assert_throws(Action&& action) {
    try {
        std::invoke(std::forward<Action>(action));
        assert(false);
    } catch (const std::exception& e) {
        assert(true);
    }
}

int main() {
    {
        const ErasedType& a = ErasableType<A>{A{1}};
        assert(erased_cast<A>(a).value == 1);
    }

    assert_throws([]() {
        const ErasedType& b = ErasableType<B>{B{}};
        std::cout << erased_cast<A>(b).value << std::endl;
    });

    return 0;
}

From my understanding, the proposed code always throws when an invalid casting occurs, and undefined behavior cannot occur. Check out the code here.

This allows also to transfer ownership to the caller, by returning an instance of std::unique_ptr<ErasedType>.

What are the possible flaws of the above implementation?

Moreover, I do not like the need for a dynamic_cast. What could be a better solution?

\$\endgroup\$
4
  • 1
    \$\begingroup\$ If the code you link to is yours and you want a good code review please include the rest of the code. The more code you include the better the review we can provide. \$\endgroup\$
    – pacmaninbw
    Commented Jun 27, 2022 at 13:37
  • \$\begingroup\$ Unfortunately, the rest of the code is not publishable. Would you need this, as a broader design change might eliminate the need for the type-erasure altogether, right? \$\endgroup\$ Commented Jun 27, 2022 at 13:50
  • 2
    \$\begingroup\$ It is true that I don't completely understand at the moment why you are doing this. Inheritance from an abstract base class might be able to solve most of the problem. \$\endgroup\$
    – pacmaninbw
    Commented Jun 27, 2022 at 14:12
  • 1
    \$\begingroup\$ You don't need to do type erasure, just pass a pointer to an opaque data type. \$\endgroup\$
    – G. Sliepen
    Commented Jun 28, 2022 at 10:25

0

Browse other questions tagged or ask your own question.