Search code examples
c++type-erasurec++17stdany

Type erasing type erasure, `any` questions?


So, suppose I want to type erase using type erasure.

I can create pseudo-methods for variants that enable a natural:

pseudo_method print = [](auto&& self, auto&& os){ os << self; };

std::variant<A,B,C> var = // create a variant of type A B or C

(var->*print)(std::cout); // print it out without knowing what it is

My question is, how do I extend this to a std::any?

It cannot be done "in the raw". But at the point where we assign to/construct a std::any we have the type information we need.

So, in theory, an augmented any:

template<class...OperationsToTypeErase>
struct super_any {
  std::any data;
  // or some transformation of OperationsToTypeErase?
  std::tuple<OperationsToTypeErase...> operations;
  // ?? what for ctor/assign/etc?
};

could somehow automatically rebind some code such that the above type of syntax would work.

Ideally it would be as terse in use as the variant case is.

template<class...Ops, class Op,
  // SFINAE filter that an op matches:
  std::enable_if_t< std::disjunction< std::is_same<Ops, Op>... >{}, int>* =nullptr
>
decltype(auto) operator->*( super_any<Ops...>& a, any_method<Op> ) {
  return std::get<Op>(a.operations)(a.data);
}

Now can I keep this to a type, yet reasonably use the lambda syntax to keep things simple?

Ideally I want:

any_method<void(std::ostream&)> print =
  [](auto&& self, auto&& os){ os << self; };

using printable_any = make_super_any<&print>;

printable_any bob = 7; // sets up the printing data attached to the any

int main() {
  (bob->*print)(std::cout); // prints 7
  bob = 3.14159;
  (bob->*print)(std::cout); // prints 3.14159
}

or similar syntax. Is this impossible? Infeasible? Easy?


Solution

  • Here's my solution. It looks shorter than Yakk's, and it does not use std::aligned_storage and placement new. It additionally supports stateful and local functors (which implies that it might never be possible to write super_any<&print>, since print could be a local variable).

    any_method:

    template<class F, class Sig> struct any_method;
    
    template<class F, class Ret, class... Args> struct any_method<F,Ret(Args...)> {
      F f;
      template<class T>
      static Ret invoker(any_method& self, boost::any& data, Args... args) {
        return self.f(boost::any_cast<T&>(data), std::forward<Args>(args)...);
      }
      using invoker_type = Ret (any_method&, boost::any&, Args...);
    };
    

    make_any_method:

    template<class Sig, class F>
    any_method<std::decay_t<F>,Sig> make_any_method(F&& f) {
      return { std::forward<F>(f) };
    }
    

    super_any:

    template<class...OperationsToTypeErase>
    struct super_any {
      boost::any data;
      std::tuple<typename OperationsToTypeErase::invoker_type*...> operations = {};
    
      template<class T, class ContainedType = std::decay_t<T>>
      super_any(T&& t)
        : data(std::forward<T>(t))
        , operations((OperationsToTypeErase::template invoker<ContainedType>)...)
      {}
    
      template<class T, class ContainedType = std::decay_t<T>>
      super_any& operator=(T&& t) {
        data = std::forward<T>(t);
        operations = { (OperationsToTypeErase::template invoker<ContainedType>)... };
        return *this;
      }
    };
    

    operator->*:

    template<class...Ops, class F, class Sig,
      // SFINAE filter that an op matches:
      std::enable_if_t< std::disjunction< std::is_same<Ops, any_method<F,Sig>>... >{}, int> = 0
    >
    auto operator->*( super_any<Ops...>& a, any_method<F,Sig> f) {
      auto fptr = std::get<typename any_method<F,Sig>::invoker_type*>(a.operations);
      return [fptr,f, &a](auto&&... args) mutable {
        return fptr(f, a.data, std::forward<decltype(args)>(args)...);
      };
    }
    

    Usage:

    #include <iostream>
    auto print = make_any_method<void(std::ostream&)>(
      [](auto&& self, auto&& os){ os << self; }
    );
    
    using printable_any = super_any<decltype(print)>;
    
    printable_any bob = 7; // sets up the printing data attached to the any
    
    int main() {
      (bob->*print)(std::cout); // prints 7
      bob = 3.14159;
      (bob->*print)(std::cout); // prints 3.14159
    }
    

    Live