サーチ…
前書き
型消去とは、基になる型情報をクライアントから隠しながら、さまざまな基底型との一貫したインタフェースを提供できる型を作成するための技法のセットです。 C ++では、さまざまなタイプの呼び出し可能なオブジェクトを保持する能力を持つstd::function<R(A...)>
、おそらく最もよく知られた型消去の例です。
基本的な仕組み
型の消去は、オブジェクトが共通の基底クラスから派生していなくても、オブジェクトを使用するコードからオブジェクトの型を隠す方法です。そうすることで、静的ポリモーフィズムの世界(テンプレート;使用場所では、コンパイル時に正確な型を知る必要がありますが、定義時にインタフェースに準拠するよう宣言する必要はありません)と動的多型(継承および仮想関数;使用場所では、コンパイル時に正確な型を知る必要はなく、定義時にインタフェースに準拠するように宣言する必要があります)。
次のコードは、タイプ消去の基本的なメカニズムを示しています。
#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;
};
使用サイトでは、仮想関数を持つ基本クラスと同様に、上記の定義のみを表示する必要があります。例えば:
#include <iostream>
void print_value(Printable const &p)
{
p.print(std::cout);
}
これはテンプレートではなく 、ヘッダーファイルで宣言するだけで、実装ファイルで定義できる通常の関数であることに注意してください(定義が使用場所で表示可能なテンプレートとは異なります)。
具体的な型の定義では、 Printable
について知る必要はありません。テンプレートと同様に、インタフェースに準拠する必要があります。
struct MyType { int i; };
ostream& operator << (ostream &os, MyType const &mc)
{
return os << "MyType {" << mc.i << "}";
}
このクラスのオブジェクトを上で定義した関数に渡すことができます:
MyType foo = { 42 };
print_value(foo);
手動vtableでレギュラータイプに消去する
C ++は、Regular型(または少なくともPseudo-Regular型)として知られています。
レギュラータイプは、コピーや移動を介して構築、割り当て、割り当てが可能なタイプであり、破壊することができ、等しいと比較することができます。引数なしで構築することもできます。最後に、さまざまなstd
アルゴリズムやコンテナで非常に有用ないくつかの操作もサポートしています。
これはルート・ペーパーですが、C ++ 11ではstd::hash
サポートを追加したいと考えています。
ここでは、手動でvtable方式で消去を入力します。
using dtor_unique_ptr = std::unique_ptr<void, void(*)(void*)>;
template<class T, class...Args>
dtor_unique_ptr make_dtor_unique_ptr( Args&&... args ) {
return {new T(std::forward<Args>(args)...), [](void* self){ delete static_cast<T*>(self); }};
}
struct regular_vtable {
void(*copy_assign)(void* dest, void const* src); // T&=(T const&)
void(*move_assign)(void* dest, void* src); // T&=(T&&)
bool(*equals)(void const* lhs, void const* rhs); // T const&==T const&
bool(*order)(void const* lhs, void const* rhs); // std::less<T>{}(T const&, T const&)
std::size_t(*hash)(void const* self); // std::hash<T>{}(T const&)
std::type_info const&(*type)(); // typeid(T)
dtor_unique_ptr(*clone)(void const* self); // T(T const&)
};
template<class T>
regular_vtable make_regular_vtable() noexcept {
return {
[](void* dest, void const* src){ *static_cast<T*>(dest) = *static_cast<T const*>(src); },
[](void* dest, void* src){ *static_cast<T*>(dest) = std::move(*static_cast<T*>(src)); },
[](void const* lhs, void const* rhs){ return *static_cast<T const*>(lhs) == *static_cast<T const*>(rhs); },
[](void const* lhs, void const* rhs) { return std::less<T>{}(*static_cast<T const*>(lhs),*static_cast<T const*>(rhs)); },
[](void const* self){ return std::hash<T>{}(*static_cast<T const*>(self)); },
[]()->decltype(auto){ return typeid(T); },
[](void const* self){ return make_dtor_unique_ptr<T>(*static_cast<T const*>(self)); }
};
}
template<class T>
regular_vtable const* get_regular_vtable() noexcept {
static const regular_vtable vtable=make_regular_vtable<T>();
return &vtable;
}
struct regular_type {
using self=regular_type;
regular_vtable const* vtable = 0;
dtor_unique_ptr ptr{nullptr, [](void*){}};
bool empty() const { return !vtable; }
template<class T, class...Args>
void emplace( Args&&... args ) {
ptr = make_dtor_unique_ptr<T>(std::forward<Args>(args)...);
if (ptr)
vtable = get_regular_vtable<T>();
else
vtable = nullptr;
}
friend bool operator==(regular_type const& lhs, regular_type const& rhs) {
if (lhs.vtable != rhs.vtable) return false;
return lhs.vtable->equals( lhs.ptr.get(), rhs.ptr.get() );
}
bool before(regular_type const& rhs) const {
auto const& lhs = *this;
if (!lhs.vtable || !rhs.vtable)
return std::less<regular_vtable const*>{}(lhs.vtable,rhs.vtable);
if (lhs.vtable != rhs.vtable)
return lhs.vtable->type().before(rhs.vtable->type());
return lhs.vtable->order( lhs.ptr.get(), rhs.ptr.get() );
}
// technically friend bool operator< that calls before is also required
std::type_info const* type() const {
if (!vtable) return nullptr;
return &vtable->type();
}
regular_type(regular_type&& o):
vtable(o.vtable),
ptr(std::move(o.ptr))
{
o.vtable = nullptr;
}
friend void swap(regular_type& lhs, regular_type& rhs){
std::swap(lhs.ptr, rhs.ptr);
std::swap(lhs.vtable, rhs.vtable);
}
regular_type& operator=(regular_type&& o) {
if (o.vtable == vtable) {
vtable->move_assign(ptr.get(), o.ptr.get());
return *this;
}
auto tmp = std::move(o);
swap(*this, tmp);
return *this;
}
regular_type(regular_type const& o):
vtable(o.vtable),
ptr(o.vtable?o.vtable->clone(o.ptr.get()):dtor_unique_ptr{nullptr, [](void*){}})
{
if (!ptr && vtable) vtable = nullptr;
}
regular_type& operator=(regular_type const& o) {
if (o.vtable == vtable) {
vtable->copy_assign(ptr.get(), o.ptr.get());
return *this;
}
auto tmp = o;
swap(*this, tmp);
return *this;
}
std::size_t hash() const {
if (!vtable) return 0;
return vtable->hash(ptr.get());
}
template<class T,
std::enable_if_t< !std::is_same<std::decay_t<T>, regular_type>{}, int>* =nullptr
>
regular_type(T&& t) {
emplace<std::decay_t<T>>(std::forward<T>(t));
}
};
namespace std {
template<>
struct hash<regular_type> {
std::size_t operator()( regular_type const& r )const {
return r.hash();
}
};
template<>
struct less<regular_type> {
bool operator()( regular_type const& lhs, regular_type const& rhs ) const {
return lhs.before(rhs);
}
};
}
ライブの例 。
このような定期的なタイプはのためのキーとして使用することができますstd::map
かstd::unordered_map
のように、キーのための定期的な何かを受け付けます。
std::map<regular_type, std::any>
基本的には、アニメのレギュラーからコピー可能なものまでのマップです。
any
とは異なり、私のregular_type
は小さなオブジェクトの最適化も、元のデータの取得をサポートしていません。元のタイプを元に戻すことは難しくありません。
小規模なオブジェクトの最適化では、整列されたストレージバッファをregular_type
内に格納し、 ptr
削除者を注意深く調整してオブジェクトを破棄し、削除しないようにする必要があります。
私はmake_dtor_unique_ptr
でmake_dtor_unique_ptr
、時にバッファにデータを格納する方法を教えて、バッファに空きがなければヒープに入れます。それで十分かもしれません。
移動のみの `std :: function`
std::function
型はいくつかの操作を削除します。必要なものの1つは、保存された値がコピー可能であることです。
これは、固有のptrsを格納するlambdaのようないくつかのコンテキストで問題を引き起こします。スレッドにタスクをディスパッチするスレッドプールのように、コピーが問題にならないコンテキストでstd::function
を使用している場合、この要件によってオーバーヘッドが増加する可能性があります。
特に、 std::packaged_task<Sig>
は、移動可能な呼び出し可能オブジェクトです。あなたはstd::packaged_task<void(Args...)>
にstd::packaged_task<R(Args...)>
を格納することができますが、それはかなり重く、あまり移動しない呼び出し可能型消去クラス。
したがって、 task
。これは、単純なstd::function
型の記述方法を示しています。コピーコンストラクタを省略しました(これには、 clone
メソッドをdetails::task_pimpl<...>
追加する必要があります)。
template<class Sig>
struct task;
// putting it in a namespace allows us to specialize it nicely for void return value:
namespace details {
template<class R, class...Args>
struct task_pimpl {
virtual R invoke(Args&&...args) const = 0;
virtual ~task_pimpl() {};
virtual const std::type_info& target_type() const = 0;
};
// store an F. invoke(Args&&...) calls the f
template<class F, class R, class...Args>
struct task_pimpl_impl:task_pimpl<R,Args...> {
F f;
template<class Fin>
task_pimpl_impl( Fin&& fin ):f(std::forward<Fin>(fin)) {}
virtual R invoke(Args&&...args) const final override {
return f(std::forward<Args>(args)...);
}
virtual const std::type_info& target_type() const final override {
return typeid(F);
}
};
// the void version discards the return value of f:
template<class F, class...Args>
struct task_pimpl_impl<F,void,Args...>:task_pimpl<void,Args...> {
F f;
template<class Fin>
task_pimpl_impl( Fin&& fin ):f(std::forward<Fin>(fin)) {}
virtual void invoke(Args&&...args) const final override {
f(std::forward<Args>(args)...);
}
virtual const std::type_info& target_type() const final override {
return typeid(F);
}
};
};
template<class R, class...Args>
struct task<R(Args...)> {
// semi-regular:
task()=default;
task(task&&)=default;
// no copy
private:
// aliases to make some SFINAE code below less ugly:
template<class F>
using call_r = std::result_of_t<F const&(Args...)>;
template<class F>
using is_task = std::is_same<std::decay_t<F>, task>;
public:
// can be constructed from a callable F
template<class F,
// that can be invoked with Args... and converted-to-R:
class= decltype( (R)(std::declval<call_r<F>>()) ),
// and is not this same type:
std::enable_if_t<!is_task<F>{}, int>* = nullptr
>
task(F&& f):
m_pImpl( make_pimpl(std::forward<F>(f)) )
{}
// the meat: the call operator
R operator()(Args... args)const {
return m_pImpl->invoke( std::forward<Args>(args)... );
}
explicit operator bool() const {
return (bool)m_pImpl;
}
void swap( task& o ) {
std::swap( m_pImpl, o.m_pImpl );
}
template<class F>
void assign( F&& f ) {
m_pImpl = make_pimpl(std::forward<F>(f));
}
// Part of the std::function interface:
const std::type_info& target_type() const {
if (!*this) return typeid(void);
return m_pImpl->target_type();
}
template< class T >
T* target() {
return target_impl<T>();
}
template< class T >
const T* target() const {
return target_impl<T>();
}
// compare with nullptr :
friend bool operator==( std::nullptr_t, task const& self ) { return !self; }
friend bool operator==( task const& self, std::nullptr_t ) { return !self; }
friend bool operator!=( std::nullptr_t, task const& self ) { return !!self; }
friend bool operator!=( task const& self, std::nullptr_t ) { return !!self; }
private:
template<class T>
using pimpl_t = details::task_pimpl_impl<T, R, Args...>;
template<class F>
static auto make_pimpl( F&& f ) {
using dF=std::decay_t<F>;
using pImpl_t = pimpl_t<dF>;
return std::make_unique<pImpl_t>(std::forward<F>(f));
}
std::unique_ptr<details::task_pimpl<R,Args...>> m_pImpl;
template< class T >
T* target_impl() const {
return dynamic_cast<pimpl_t<T>*>(m_pImpl.get());
}
};
このライブラリを有効にするには、小さなバッファの最適化を追加して、ヒープ上のすべての呼び出し可能コードを格納しないようにします。
SBOを追加するには、デフォルト以外のtask(task&&)
、クラス内のいくつかのstd::aligned_storage_t
、destroy-only(メモリをヒープに返さない)に設定できるdeleterを持つm_pImpl
unique_ptr
、 emplace_move_to( void* ) = 0
をtask_pimpl
ます。
上記のコードの実例 (SBOなし)。
Tの連続したバッファまで消去する
すべての型の消去に仮想継承、割り当て、配置の新機能、または関数ポインタさえも含まれます。
タイプ消去タイプの消去を行うのは、(一連の)ビヘイビアを記述し、そのビヘイビアをサポートする任意のタイプを取り、それをラップすることです。その一連の行動に含まれないすべての情報は、「忘れ去られた」または「消去された」。
array_view
は入力範囲またはコンテナ型を取り、それが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};
}
// 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()) {}
};
array_view
は、 .data()
をサポートする任意のコンテナを、 T
および.size()
メソッドへのポインタまたは配列をarray_view
取り、それを連続したT
sのランダムアクセス範囲になるまでarray_view
ます。
これはstd::vector<T>
、 std::string<T>
、 std::array<T, N>
T[37]
、イニシャライザリスト( {}
基づくものを含む)、またはその他のものを取ることができます( T* x.data()
とsize_t x.size()
T* x.data()
サポートしています。
この場合、私たちが消去しているものから抽出することができるデータは、「ビュー」非所有状態とともに、メモリを割り当てる必要もなく、カスタム型依存関数を書く必要もありません。
実例 。
ADLが有効なコンテキストで非メンバ・data
と非メンバ・size
を使用することが改善size
れます。
タイプを消去するタイプ消去:std :: any
この例では、C ++ 14とboost::any
ます。 C ++ 17ではstd::any
代わりにstd::any
スワップできます。
最終的な構文は次のとおりです。
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);
これはほぼ最適です。
この例は、 @dypと@cpplearnerの仕事と私自身の仕事に基づいています。
まず、型を渡すタグを使用します。
template<class T>struct tag_t{constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag{};
この特性クラスは、 any_method
保存されたシグネチャを取得します。
これは、関数ポインタ型とその関数ポインタのファクトリを作成し、 any_method
与え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
は、インスタンスと一緒に格納する関数ポインタの型です。 any_method_function::operator()
はtag_t<T>
をとり、 tag_t<T>
any&
T
となることを前提としたany_method_function::type
カスタムインスタンスを書き込みます。
我々は、一度に複数のメソッドをタイプイレースできるようにしたい。そのため、タプルでそれらを束ね、型ごとに静的ストレージにタプルを貼り付け、それらへのポインタを保持するヘルパーラッパーを作成します。
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;
}
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 );
}
};
vtableが小さい場合(たとえば1項目)にはこの機能を特化し、そのような場合は効率のためにクラス内に格納された直接ポインタを使用することができます。
今度はsuper_any
を開始しsuper_any
。私はsuper_any_t
を使って、 super_any
の宣言を簡単にしています。
template<class...methods>
struct super_any_t;
これは、スーパーがSFINAEとより良いエラーメッセージをサポートするメソッドを検索します。
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> ) )
{};
次に、 any_method
型を作成します。 any_method
は擬似メソッドポインタです。我々はそれをグローバルに作成し、以下のような構文を使用してconst
を作成します。
const auto print=make_any_method( [](auto&&self, auto&&os){ os << self; } );
またはC ++で17:
const any_method print=[](auto&&self, auto&&os){ os << self; };
非ラムダを使用すると、ルックアップステップのために型を使用するので、物を毛深くすることができることに注意してください。これは修正することができますが、この例は既にそれよりも長くなります。だから、常にラムダから、またはラムダでパラメータ化された型から、任意のメソッドを初期化します。
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)...);
}
};
C ++で必要とされないファクトリメソッド17私は信じています:
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)};
}
これは増補されたany
です。これは、両方でany
、そしてそれが含まれている時はいつでも変更型消去関数ポインタの束の周りに運ぶany
行います。
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>;
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;
}
};
any_method
をconst
オブジェクトとして格納するので、これによりsuper_any
が少し簡単になります。
template<class...Ts>
using super_any = super_any_t< std::remove_cv_t<Ts>... >;
テストコード:
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);
}
ライブの例 。
もともとは、SOの自己質問と回答(そして、実装に役立った上記の人々)に投稿されました。