Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
53
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

Small object optimization requires that we store an aligned storage bu er within the regular_type, and carefully tweak the deleter of the ptr to only destroy the object and not delete it.

I would start at make_dtor_unique_ptr and teach it how to sometimes store the data in a bu er, and then in the heap if no room in the bu er. That may be su cient.

Section 90.3: Basic mechanism

Type erasure is a way to hide the type of an object from code using it, even though it is not derived from a common base class. In doing so, it provides a bridge between the worlds of static polymorphism (templates; at the place of use, the exact type must be known at compile time, but it need not be declared to conform to an interface at definition) and dynamic polymorphism (inheritance and virtual functions; at the place of use, the exact type need not be known at compile time, but must be declared to conform to an interface at definition).

The following code shows the basic mechanism of type erasure.

#include <ostream>

class Printable

{

public:

template <typename T>

Printable(T value) : pValue(new Value<T>(value)) {} ~Printable() { delete pValue; }

void print(std::ostream &os) const { pValue->print(os); }

private:

Printable(Printable const &) /* in C++1x: =delete */; // not implemented void operator = (Printable const &) /* in C++1x: =delete */; // not implemented struct ValueBase

{

virtual ~ValueBase() = default;

virtual void print(std::ostream &) const = 0;

};

template <typename T> struct Value : ValueBase

{

Value(T const &t) : v(t) {}

virtual void print(std::ostream &os) const { os << v; } T v;

};

ValueBase *pValue;

};

At the use site, only the above definition need to be visible, just as with base classes with virtual functions. For example:

#include <iostream>

void print_value(Printable const &p)

{

p.print(std::cout);

}

Note that this is not a template, but a normal function that only needs to be declared in a header file, and can be defined in an implementation file (unlike templates, whose definition must be visible at the place of use).

At the definition of the concrete type, nothing needs to be known about Printable, it just needs to conform to an

GoalKicker.com – C++ Notes for Professionals

475

interface, as with templates:

struct MyType { int i; };

ostream& operator << (ostream &os, MyType const &mc)

{

return os << "MyType {" << mc.i << "}";

}

We can now pass an object of this class to the function defined above:

MyType foo = { 42 }; print_value(foo);

Section 90.4: Erasing down to a contiguous bu er of T

Not all type erasure involves virtual inheritance, allocations, placement new, or even function pointers.

What makes type erasure type erasure is that it describes a (set of) behavior(s), and takes any type that supports that behavior and wraps it up. All information that isn't in that set of behaviors is "forgotten" or "erased".

An array_view takes its incoming range or container type and erases everything except the fact it is a contiguous bu er of T.

// helper traits for SFINAE: template<class T>

using data_t = decltype( std::declval<T>().data() );

template<class Src, class T>

using compatible_data = std::integral_constant<bool, std::is_same< data_t<Src>, T* >{} || std::is_same< data_t<Src>, std::remove_const_t<T>* >{}>;

template<class T> struct array_view {

//the core of the class: T* b=nullptr;

T* e=nullptr;

T* begin() const { return b; } T* end() const { return e; }

//provide the expected methods of a good contiguous range: T* data() const { return begin(); }

bool empty() const { return begin()==end(); } std::size_t size() const { return end()-begin(); }

T& operator[](std::size_t i)const{ return begin()[i]; }

T& front()const{ return *begin(); }

T& back()const{ return *(end()-1); }

//useful helpers that let you generate other ranges from this one

//quickly and safely:

array_view without_front( std::size_t i=1 ) const { i = (std::min)(i, size());

return {begin()+i, end()};

}

array_view without_back( std::size_t i=1 ) const { i = (std::min)(i, size());

return {begin(), end()-i};

}

GoalKicker.com – C++ Notes for Professionals

476

//array_view is plain old data, so default copy: array_view(array_view const&)=default;

//generates a null, empty range:

array_view()=default;

// final constructor:

array_view(T* s, T* f):b(s),e(f) {}

// start and length is useful in my experience:

array_view(T* s, std::size_t length):array_view(s, s+length) {}

// SFINAE constructor that takes any .data() supporting container

//or other range in one fell swoop: template<class Src,

std::enable_if_t< compatible_data<std::remove_reference_t<Src>&, T >{}, int>* =nullptr, std::enable_if_t< !std::is_same<std::decay_t<Src>, array_view >{}, int>* =nullptr

>

array_view( Src&& src ):

array_view( src.data(), src.size() )

{}

//array constructor:

template<std::size_t N>

array_view( T(&arr)[N] ):array_view(arr, N) {}

// initializer list, allowing {} based: template<class U,

std::enable_if_t< std::is_same<const U, T>{}, int>* =nullptr

>

array_view( std::initializer_list<U> il ):array_view(il.begin(), il.end()) {}

};

an array_view takes any container that supports .data() returning a pointer to T and a .size() method, or an array, and erases it down to being a random-access range over contiguous Ts.

It can take a std::vector<T>, a std::string<T> a std::array<T, N> a T[37], an initializer list (including {} based ones), or something else you make up that supports it (via T* x.data() and size_t x.size()).

In this case, the data we can extract from the thing we are erasing, together with our "view" non-owning state, means we don't have to allocate memory or write custom type-dependent functions.

Live example.

An improvement would be to use a non-member data and a non-member size in an ADL-enabled context.

Section 90.5: Type erasing type erasure with std::any

This example uses C++14 and boost::any. In C++17 you can swap in std::any instead.

The syntax we end up with is:

const auto print =

make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });

super_any<decltype(print)> a = 7;

(a->*print)(std::cout);

which is almost optimal.

GoalKicker.com – C++ Notes for Professionals

477

This example is based o of work by @dyp and @cpplearner as well as my own.

First we use a tag to pass around types:

template<class T>struct tag_t{constexpr tag_t(){};}; template<class T>constexpr tag_t<T> tag{};

This trait class gets the signature stored with an any_method:

This creates a function pointer type, and a factory for said function pointers, given an any_method:

template<class any_method>

using any_sig_from_method = typename any_method::signature;

template<class any_method, class Sig=any_sig_from_method<any_method>> struct any_method_function;

template<class any_method, class R, class...Args> struct any_method_function<any_method, R(Args...)>

{

template<class T>

using decorate = std::conditional_t< any_method::is_const, T const, T >; using any = decorate<boost::any>;

using type = R(*)(any&, any_method const*, Args&&...); template<class T>

type operator()( tag_t<T> )const{

return +[](any& self, any_method const* method, Args&&...args) {

return (*method)( boost::any_cast<decorate<T>&>(self), decltype(args)(args)... );

};

}

};

any_method_function::type is the type of a function pointer we will store alongside the instance.

any_method_function::operator() takes a tag_t<T> and writes a custom instance of the

any_method_function::type that assumes the any& is going to be a T.

We want to be able to type-erase more than one method at a time. So we bundle them up in a tuple, and write a helper wrapper to stick the tuple into static storage on a per-type basis and maintain a pointer to them.

template<class...any_methods>

using any_method_tuple = std::tuple< typename any_method_function<any_methods>::type... >;

template<class...any_methods, class T> any_method_tuple<any_methods...> make_vtable( tag_t<T> ) {

return std::make_tuple( any_method_function<any_methods>{}(tag<T>)...

);

}

template<class...methods> struct any_methods { private:

any_method_tuple<methods...> const* vtable = 0; template<class T>

static any_method_tuple<methods...> const* get_vtable( tag_t<T> ) { static const auto table = make_vtable<methods...>(tag<T>);

return &table;

GoalKicker.com – C++ Notes for Professionals

478

}

public:

any_methods() = default; template<class T>

any_methods( tag_t<T> ): vtable(get_vtable(tag<T>)) {} any_methods& operator=(any_methods const&)=default; template<class T>

void change_type( tag_t<T> ={} ) { vtable = get_vtable(tag<T>); }

template<class any_method>

auto get_invoker( tag_t<any_method> ={} ) const {

return std::get<typename any_method_function<any_method>::type>( *vtable );

}

};

We could specialize this for a cases where the vtable is small (for example, 1 item), and use direct pointers stored in-class in those cases for e ciency.

Now we start the super_any. I use super_any_t to make the declaration of super_any a bit easier.

template<class...methods> struct super_any_t;

This searches the methods that the super any supports for SFINAE and better error messages:

template<class super_any, class method>

struct super_method_applies_helper : std::false_type {};

template<class M0, class...Methods, class method>

struct super_method_applies_helper<super_any_t<M0, Methods...>, method> : std::integral_constant<bool, std::is_same<M0, method>{} ||

super_method_applies_helper<super_any_t<Methods...>, method>{}> {};

template<class...methods, class method>

auto super_method_test( super_any_t<methods...> const&, tag_t<method> )

{

return std::integral_constant<bool, super_method_applies_helper< super_any_t<methods...>, method >{} && method::is_const >{};

}

template<class...methods, class method>

auto super_method_test( super_any_t<methods...>&, tag_t<method> )

{

return std::integral_constant<bool, super_method_applies_helper< super_any_t<methods...>, method >{} >{};

}

template<class super_any, class method> struct super_method_applies:

decltype( super_method_test( std::declval<super_any>(), tag<method> ) )

{};

Next we create the any_method type. An any_method is a pseudo-method-pointer. We create it globally and constly using syntax like:

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

or in C++17:

GoalKicker.com – C++ Notes for Professionals

479

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

Note that using a non-lambda can make things hairy, as we use the type for a lookup step. This can be fixed, but would make this example longer than it already is. So always initialize an any method from a lambda, or from a type parametarized on a lambda.

template<class Sig, bool const_method, class F> struct any_method {

using signature=Sig; enum{is_const=const_method};

private: F f; public:

template<class Any,

// SFINAE testing that one of the Anys's matches this type: std::enable_if_t< super_method_applies< Any&&, any_method >{}, int>* =nullptr

>

friend auto operator->*( Any&& self, any_method const& m ) {

//we don't use the value of the any_method, because each any_method has

//a unique type (!) and we check that one of the auto*'s in the super_any

//already has a pointer to us. We then dispatch to the corresponding

//any_method_data...

return [&self, invoke = self.get_invoker(tag<any_method>), m](auto&&...args)->decltype(auto)

{

return invoke( decltype(self)(self), &m, decltype(args)(args)... );

};

}

any_method( F fin ):f(std::move(fin)) {}

template<class...Args>

decltype(auto) operator()(Args&&...args)const { return f(std::forward<Args>(args)...);

}

};

A factory method, not needed in C++17 I believe:

template<class Sig, bool is_const=false, class F> any_method<Sig, is_const, std::decay_t<F>> make_any_method( F&& f ) {

return {std::forward<F>(f)};

}

This is the augmented any. It is both an any, and it carries around a bundle of type-erasure function pointers that change whenever the contained any does:

template<class... methods>

struct super_any_t:boost::any, any_methods<methods...> { using vtable=any_methods<methods...>;

public: template<class T,

std::enable_if_t< !std::is_base_of<super_any_t, std::decay_t<T>>{}, int> =0

>

super_any_t( T&& t ):

boost::any( std::forward<T>(t) )

{

using dT=std::decay_t<T>;

GoalKicker.com – C++ Notes for Professionals

480

this->change_type( tag<dT> );

}

boost::any& as_any()&{return *this;} boost::any&& as_any()&&{return std::move(*this);} boost::any const& as_any()const&{return *this;} super_any_t()=default;

super_any_t(super_any_t&& o): boost::any( std::move( o.as_any() ) ), vtable(o)

{}

super_any_t(super_any_t const& o): boost::any( o.as_any() ), vtable(o)

{} template<class S,

std::enable_if_t< std::is_same<std::decay_t<S>, super_any_t>{}, int> =0

>

super_any_t( S&& o ):

boost::any( std::forward<S>(o).as_any() ), vtable(o)

{}

super_any_t& operator=(super_any_t&&)=default; super_any_t& operator=(super_any_t const&)=default;

template<class T,

std::enable_if_t< !std::is_same<std::decay_t<T>, super_any_t>{}, int>* =nullptr

>

super_any_t& operator=( T&& t ) { ((boost::any&)*this) = std::forward<T>(t); using dT=std::decay_t<T>; this->change_type( tag<dT> );

return *this;

}

};

Because we store the any_methods as const objects, this makes making a super_any a bit easier:

template<class...Ts>

using super_any = super_any_t< std::remove_cv_t<Ts>... >;

Test code:

const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });

const auto wprint = make_any_method<void(std::wostream&)>([](auto&& p, std::wostream& os ){ os << p << L"\n"; });

int main()

{

super_any<decltype(print), decltype(wprint)> a = 7; super_any<decltype(print), decltype(wprint)> a2 = 7;

(a->*print)(std::cout); (a->*wprint)(std::wcout);

}

live example.

Originally posted here in a SO self question & answer (and people noted above helped with the implementation).

GoalKicker.com – C++ Notes for Professionals

481