6
\$\begingroup\$

I've been using std::unique_ptr for cases where I want to store a derived-type value with type erasure, and felt that unique_ptr being nullable was an undesirable degree of freedom. From Polymorphic (owned) reference wrapper for class hierarchies I understand Adobe poly and boost probably have similar features, but I'm not familiar with poly, and boost is heavy. So, I've made a value-semantic class that implements type erasure for C++17. It can be converted to C++11 by changing the std::enable_if_t and similar. It supports copy construction, type-safe deletion, and assignment operators. It's different from std::any in allowing use of the -> and * operators with sound polymorphism. One thing I'm not sure about is the "perfect forwarding" because the examples I see use Args&&..., but I was not able to get that to compile. There are usage examples at the end.

#ifndef ERASED_HPP
#define ERASED_HPP

#include <functional>
#include <memory>

// value semantic type erasure via base types
template<typename T>
class erased final {
public:
    // alias for unique_ptr with a deleter function
    template<typename T>
    using ptr = std::unique_ptr<T, std::function<void (T*)>>;

    // construct with perfect forwarding to T
    template<typename... Args>
    erased(Args... args) : cloner(create_cloner<T>()), deleter(create_deleter<T>()), value(new T(std::forward<Args>(args)...), deleter) {}

    // construct from type that inherits T
    template<typename U>
    erased(U const & value) : cloner(create_cloner<U>()), deleter(create_deleter<U>()), value(new U(value), deleter) {
        static_assert(std::is_base_of<T, U>::value, "the given value does not inherit the erasure type");
        static_assert(std::is_copy_constructible<U>::value, "the given value is not copy constructible");
    }

    // copy constructor
    erased(erased const & other) : cloner(other.cloner), deleter(other.deleter), value(cloner(&*other.value), deleter) {}

    // construct from erased<U> where U inherits T
    template<typename U>
    erased(erased<U> const & other) : cloner(create_cloner<U>(other.cloner)), deleter(create_deleter<U>(other.deleter)), value(other.cloner(&*other.value), deleter)  {
        static_assert(std::is_base_of<T, U>::value, "the given value does not inherit the erasure type");
    }

    // assignment operator
    erased & operator =(erased const & other) {
        cloner = other.cloner;
        deleter = other.deleter;
        value.reset(cloner(&*other.value), deleter);
        return *this;
    }

    // assignment operator from erased<U> where U inherits T
    template<typename U>
    erased & operator =(erased<U> const & other) {
        static_assert(std::is_base_of<T, U>::value, "the given value does not inherit the erasure type");
        cloner = create_cloner<U>(other.cloner);
        deleter = create_deleter<U>(other.deleter);
        ptr<T> newu_p(other.cloner(&*other.value), deleter)
        value.swap(std::move(newu_p));
        return *this;
    }

    // assignment from U where U inherits T, and U is not erased<U>
    template<typename U, typename std::enable_if_t<std::is_base_of_v<T, U>, int> = 0>
    erased & operator =(U const & newValue) {
        cloner = create_cloner<U>();
        deleter = create_deleter<U>();
        ptr<T> newu_p(new U(newValue), deleter);
        value.swap(std::move(newu_p));
        return *this;
    }

    // assignment from U where T can be assigned by U, and U is not an inheritor of T
    template<typename U, typename std::enable_if_t<!std::is_base_of_v<T, U>, int> = 0>
    erased & operator =(U const & newValue) {
        *value = newValue;
        return *this;
    }

    T & operator *() { return *value; }
    T * operator ->() { return &*value; }
    T const & operator *() const { return *value; }
    T const * operator ->() const { return &*value; }

private:
    template<typename U>
    std::function<T* (T*)> create_cloner() {
        return [](T * original) {
            return (T*)new U(*static_cast<U*>(original));
        };
    }

    // create a cloner function of a multiply-erased value by wrapping the cloner of the source erased<U>
    template<typename U>
    std::function<T* (T*)> create_cloner(std::function<U* (U*)> underlying) {
        return [underlying](T * original) {
            return (T*)underlying(static_cast<U*>(original));
        };
    }

    template<typename U>
    std::function<void (T*)> create_deleter() {
        return [](T * p) {
            delete (U*)p;
        };
    }

    // create a deleter function of a multiply-erased value by wrapping the cloner of the source erased<U>
    template<typename U>
    std::function<void (T*)> create_deleter(std::function<void (U*)> underlying) {
        return [underlying](T * p) {
            underlying(static_cast<U*>(p));
        };
    }

    template<typename U>
    friend class erased;

    std::function<T* (T*)> cloner;
    std::function<void(T*)> deleter;
    ptr<T> value;
};

#endif //ERASED_HPP

And the examples/test cases:

#include <cassert>
#include <iostream>

#include "erased.hpp"

class base {
public:
    virtual ~base() = default;
    base() : value(1) {}
    base(int value) : value(value) {}
    int value;
};

class derived_a : public base {
public:
    derived_a() {}
    derived_a & operator =(int newValue) { value = newValue; return *this; }
    derived_a(int value) : base(value) {}
};

class derived_b : public derived_a {
public:
    derived_b() : derived_a(), lower_value(2) {}
    derived_b(int value, int lowerValue) : derived_a(value), lower_value(lowerValue) {}
    int lower_value;
};

void test1() {
    erased<derived_b> derivedValue;
    erased<derived_a> test(derivedValue);
    erased<base> test2(test);

    assert(test2->value = 1);
    derived_b * test3 = static_cast<derived_b *>(&*test2);
    assert(test3->lower_value == 2);
}

void test2() {
    derived_b derivedValue(3, 4);

    erased<derived_a> test(derivedValue);
    erased<base> test2(test);

    assert(test2->value = 3);
    derived_b * test3 = static_cast<derived_b *>(&*test2);
    assert(test3->lower_value == 4);
}

void test3() {
    erased<derived_b> derivedValue(5, 6);
    erased<derived_a> test(derivedValue);
    erased<base> test2(test);

    assert(test2->value = 5);
    derived_b * test3 = static_cast<derived_b *>(&*test2);
    assert(test3->lower_value == 6);
}

void test4() {
    erased<derived_b> derivedValue;
    derivedValue = derived_b(7, 8);
    erased<derived_a> test(derivedValue);
    erased<base> test2(test);

    assert(test2->value = 7);
    derived_b * test3 = static_cast<derived_b *>(&*test2);
    assert(test3->lower_value == 8);
}

void test5() {
    erased<derived_a> derivedValue;
    derivedValue = derived_b(9, 10);
    erased<base> test2(derivedValue);

    assert(test2->value = 9);
    derived_b * test3 = static_cast<derived_b *>(&*test2);
    assert(test3->lower_value == 10);
}

void test6() {
    erased<derived_a> derivedValue;
    derivedValue = 11;
    erased<base> test2(derivedValue);

    assert(test2->value = 11);
}

int main()
{
    test1();
    test2();
    test3();
    test4();
    test5();
    test6();
    return 0;
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Is it like std::any? \$\endgroup\$ – Incomputable Jun 4 '17 at 8:26
  • \$\begingroup\$ Not quite. It takes a single template parameter of the base class that you want to hold. \$\endgroup\$ – Brent Jun 4 '17 at 15:16
  • \$\begingroup\$ The std::functions cost too much as you don't need that much flexibility there. \$\endgroup\$ – Deduplicator Aug 4 '17 at 22:54
  • \$\begingroup\$ Nice catch. I've spent that last few hours addressing that, and I'll have a follow up question. \$\endgroup\$ – Brent Aug 5 '17 at 2:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.