Ketik teknik penghapusan

136

(Dengan tipe erasure, maksud saya menyembunyikan beberapa atau semua tipe informasi mengenai kelas, agak seperti Boost . Setiap .)
Saya ingin mengetahui teknik-teknik penghapusan tipe, sambil juga membagikan yang, yang saya tahu. Harapan saya agak untuk menemukan beberapa teknik gila yang dipikirkan seseorang di jam tergelapnya. :)

Yang pertama dan paling jelas, dan pendekatan yang biasa diambil, yang saya tahu, adalah fungsi virtual. Sembunyikan saja implementasi kelas Anda di dalam hierarki kelas berbasis antarmuka. Banyak pustaka Boost melakukan ini, misalnya Boost.Any melakukan ini untuk menyembunyikan tipe Anda dan Boost.Shared_ptr melakukan ini untuk menyembunyikan mekanik alokasi (de).

Lalu ada opsi dengan fungsi pointer ke fungsi templated, sambil memegang objek yang sebenarnya dalam void*pointer, seperti Boost. Fungsi yang dilakukannya untuk menyembunyikan tipe nyata dari functor. Contoh implementasi dapat ditemukan di akhir pertanyaan.

Jadi, untuk pertanyaan saya yang sebenarnya:
Apa teknik penghapusan tipe lain yang Anda ketahui? Harap berikan kepada mereka, jika mungkin, dengan kode contoh, gunakan case, pengalaman Anda dengan mereka dan mungkin tautan untuk membaca lebih lanjut.

Sunting
(Karena saya tidak yakin apakah akan menambahkan ini sebagai jawaban, atau hanya mengedit pertanyaan, saya hanya akan melakukan yang lebih aman.)
Teknik lain yang bagus untuk menyembunyikan jenis sesuatu yang sebenarnya tanpa fungsi virtual atau void*mengutak - atik, adalah satu GMan mempekerjakan di sini , dengan relevansi dengan pertanyaan saya tentang bagaimana tepatnya ini bekerja.


Kode contoh:

#include <iostream>
#include <string>

// NOTE: The class name indicates the underlying type erasure technique

// this behaves like the Boost.Any type w.r.t. implementation details
class Any_Virtual{
        struct holder_base{
                virtual ~holder_base(){}
                virtual holder_base* clone() const = 0;
        };

        template<class T>
        struct holder : holder_base{
                holder()
                        : held_()
                {}

                holder(T const& t)
                        : held_(t)
                {}

                virtual ~holder(){
                }

                virtual holder_base* clone() const {
                        return new holder<T>(*this);
                }

                T held_;
        };

public:
        Any_Virtual()
                : storage_(0)
        {}

        Any_Virtual(Any_Virtual const& other)
                : storage_(other.storage_->clone())
        {}

        template<class T>
        Any_Virtual(T const& t)
                : storage_(new holder<T>(t))
        {}

        ~Any_Virtual(){
                Clear();
        }

        Any_Virtual& operator=(Any_Virtual const& other){
                Clear();
                storage_ = other.storage_->clone();
                return *this;
        }

        template<class T>
        Any_Virtual& operator=(T const& t){
                Clear();
                storage_ = new holder<T>(t);
                return *this;
        }

        void Clear(){
                if(storage_)
                        delete storage_;
        }

        template<class T>
        T& As(){
                return static_cast<holder<T>*>(storage_)->held_;
        }

private:
        holder_base* storage_;
};

// the following demonstrates the use of void pointers 
// and function pointers to templated operate functions
// to safely hide the type

enum Operation{
        CopyTag,
        DeleteTag
};

template<class T>
void Operate(void*const& in, void*& out, Operation op){
        switch(op){
        case CopyTag:
                out = new T(*static_cast<T*>(in));
                return;
        case DeleteTag:
                delete static_cast<T*>(out);
        }
}

class Any_VoidPtr{
public:
        Any_VoidPtr()
                : object_(0)
                , operate_(0)
        {}

        Any_VoidPtr(Any_VoidPtr const& other)
                : object_(0)
                , operate_(other.operate_)
        {
                if(other.object_)
                        operate_(other.object_, object_, CopyTag);
        }

        template<class T>
        Any_VoidPtr(T const& t)
                : object_(new T(t))
                , operate_(&Operate<T>)
        {}

        ~Any_VoidPtr(){
                Clear();
        }

        Any_VoidPtr& operator=(Any_VoidPtr const& other){
                Clear();
                operate_ = other.operate_;
                operate_(other.object_, object_, CopyTag);
                return *this;
        }

        template<class T>
        Any_VoidPtr& operator=(T const& t){
                Clear();
                object_ = new T(t);
                operate_ = &Operate<T>;
                return *this;
        }

        void Clear(){
                if(object_)
                        operate_(0,object_,DeleteTag);
                object_ = 0;
        }

        template<class T>
        T& As(){
                return *static_cast<T*>(object_);
        }

private:
        typedef void (*OperateFunc)(void*const&,void*&,Operation);

        void* object_;
        OperateFunc operate_;
};

int main(){
        Any_Virtual a = 6;
        std::cout << a.As<int>() << std::endl;

        a = std::string("oh hi!");
        std::cout << a.As<std::string>() << std::endl;

        Any_Virtual av2 = a;

        Any_VoidPtr a2 = 42;
        std::cout << a2.As<int>() << std::endl;

        Any_VoidPtr a3 = a.As<std::string>();
        a2 = a3;
        a2.As<std::string>() += " - again!";
        std::cout << "a2: " << a2.As<std::string>() << std::endl;
        std::cout << "a3: " << a3.As<std::string>() << std::endl;

        a3 = a;
        a3.As<Any_Virtual>().As<std::string>() += " - and yet again!!";
        std::cout << "a: " << a.As<std::string>() << std::endl;
        std::cout << "a3->a: " << a3.As<Any_Virtual>().As<std::string>() << std::endl;

        std::cin.get();
}
Xeo
sumber
1
Dengan "type erasure", apakah Anda benar-benar mengacu pada "polimorfisme"? Saya pikir "type erasure" memiliki makna yang agak spesifik, yang biasanya dikaitkan dengan misalnya generik Java.
Oliver Charlesworth
3
@Li: Ketik penghapusan dapat diimplementasikan dengan polimorfisme, tapi itu bukan satu-satunya pilihan, contoh kedua saya menunjukkan itu. :) Dan dengan tipe erasure yang saya maksud, bahwa struct Anda tidak bergantung pada tipe template misalnya. Meningkatkan.Fungsi tidak peduli jika Anda memberinya fungsi, pointer fungsi, atau bahkan lambda. Sama dengan Boost.Shared_Ptr. Anda dapat menentukan fungsi pengalokasian dan alokasi, tetapi jenis aktualnya shared_ptrtidak mencerminkan ini, itu akan selalu sama, shared_ptr<int>misalnya, tidak seperti wadah standar.
Xeo
2
@ Matthieu: Saya menganggap contoh kedua juga ketik aman. Anda selalu tahu jenis persisnya tempat Anda beroperasi. Atau apakah saya melewatkan sesuatu?
Xeo
2
@ Matthieu: Anda benar. Biasanya Asfungsi seperti itu tidak akan diimplementasikan seperti itu. Seperti saya katakan, tidak berarti aman digunakan! :)
Xeo
4
@lurscher: Yah ... tidak pernah menggunakan dorongan atau versi std dari setiap dari berikut ini? function, shared_ptr, any, Dll? Mereka semua menggunakan tipe erasure untuk kenyamanan pengguna yang manis.
Xeo

Jawaban:

100

Semua jenis teknik penghapusan di C ++ dilakukan dengan pointer fungsi (untuk perilaku) dan void*(untuk data). Metode "berbeda" hanya berbeda dalam cara mereka menambahkan gula semantik. Fungsi virtual, misalnya, hanyalah gula semantik

struct Class {
    struct vtable {
        void (*dtor)(Class*);
        void (*func)(Class*,double);
    } * vtbl
};

iow: pointer fungsi.

Yang mengatakan, ada satu teknik yang saya terutama suka: Ini shared_ptr<void>, hanya karena itu menghancurkan pikiran orang-orang yang tidak tahu Anda bisa melakukan ini: Anda dapat menyimpan data apa pun dalam shared_ptr<void>, dan masih memiliki destructor yang benar dipanggil di akhir, karena shared_ptrkonstruktor adalah templat fungsi, dan akan menggunakan jenis objek aktual yang dilewati untuk membuat deleter secara default:

{
    const shared_ptr<void> sp( new A );
} // calls A::~A() here

Tentu saja, ini hanya void*penghapusan tipe biasa / fungsi-pointer, tetapi sangat mudah dikemas.

Marc Mutz - mmutz
sumber
9
Secara kebetulan, saya harus menjelaskan perilaku shared_ptr<void>teman saya dengan contoh implementasi beberapa hari yang lalu. :) Ini benar-benar keren.
Xeo
Jawaban yang bagus; untuk membuatnya luar biasa, sebuah sketsa tentang bagaimana vtable palsu dapat dibuat secara statis untuk setiap jenis yang terhapus sangat mendidik. Perhatikan bahwa false-vtable dan implementasi fungsi-pointer memberi Anda struktur berukuran memori yang diketahui (dibandingkan dengan tipe virtual-virtual) yang dapat dengan mudah disimpan secara lokal, dan (mudah) bercerai dari data yang divirtualkan.
Yakk - Adam Nevraumont
jadi, jika shared_ptr kemudian menyimpan Derived *, tetapi Base * tidak menyatakan destructor sebagai virtual, shared_ptr <void> masih berfungsi seperti yang dimaksudkan, karena ia bahkan tidak pernah tahu tentang kelas dasar untuk memulai. Keren!
TamaMcGlinn
@ Apollys: Ya, tetapi unique_ptrtidak mengetik-menghapus deleter, jadi jika Anda ingin menetapkan unique_ptr<T>a unique_ptr<void>, Anda perlu memberikan argumen deleter, secara eksplisit, yang tahu cara menghapus Ta void*. Jika sekarang Anda ingin menetapkan S, juga, maka Anda memerlukan deleter, secara eksplisit, yang tahu cara menghapus a Tmelalui void*dan juga Smelalui void*, dan , mengingat void*, tahu apakah itu a Tatau a S. Pada titik itu, Anda telah menulis deleter yang dihapus dengan tipe unique_ptr, dan kemudian berhasil juga unique_ptr. Tidak keluar dari kotak.
Marc Mutz - mmutz
Saya merasa pertanyaan yang Anda jawab adalah "Bagaimana cara saya mengatasi kenyataan bahwa ini tidak berhasil unique_ptr?" Berguna untuk beberapa orang, tetapi tidak menjawab pertanyaan saya. Saya kira jawabannya adalah, karena pointer bersama mendapat lebih banyak perhatian dalam pengembangan perpustakaan standar. Yang saya pikir agak sedih karena pointer unik lebih sederhana, jadi seharusnya lebih mudah untuk menerapkan fungsi dasar, dan mereka lebih efisien sehingga orang harus menggunakannya lebih banyak. Sebaliknya, kita memiliki kebalikannya.
Apollys mendukung Monica
54

Pada dasarnya, itu adalah opsi Anda: fungsi virtual atau pointer fungsi.

Bagaimana Anda menyimpan data dan mengaitkannya dengan fungsi dapat bervariasi. Sebagai contoh, Anda bisa menyimpan pointer-to-base, dan meminta kelas turunannya berisi data dan implementasi fungsi virtual, atau Anda bisa menyimpan data di tempat lain (misalnya dalam buffer yang dialokasikan secara terpisah), dan kelas turunannya menyediakan implementasi fungsi virtual, yang mengambil void*poin ke data. Jika Anda menyimpan data dalam buffer terpisah, maka Anda bisa menggunakan pointer fungsi daripada fungsi virtual.

Menyimpan pointer-ke-pangkalan berfungsi dengan baik dalam konteks ini, bahkan jika data disimpan secara terpisah, jika ada beberapa operasi yang ingin Anda terapkan pada data yang dihapus tipe Anda. Kalau tidak, Anda berakhir dengan beberapa pointer fungsi (satu untuk masing-masing fungsi yang terhapus jenis), atau berfungsi dengan parameter yang menentukan operasi yang akan dilakukan.

Anthony Williams
sumber
1
Jadi, dengan kata lain contoh yang saya berikan dalam pertanyaan? Padahal, terima kasih telah menuliskannya seperti ini, terutama yang terkait dengan fungsi virtual dan beberapa operasi pada tipe data yang terhapus.
Xeo
Setidaknya ada 2 opsi lain. Saya menyusun jawaban.
John Dibling
25

Saya juga akan mempertimbangkan (mirip dengan void*) penggunaan "raw storage": char buffer[N].

Dalam C ++ 0x yang Anda miliki std::aligned_storage<Size,Align>::typeuntuk ini.

Anda dapat menyimpan apa pun yang Anda inginkan di sana, asalkan itu cukup kecil dan Anda menangani perataan dengan benar.

Matthieu M.
sumber
4
Ya, Boost. Fungsi sebenarnya menggunakan kombinasi ini dan contoh kedua yang saya berikan. Jika functor cukup kecil, ia menyimpannya secara internal di dalam functor_buffer. Senang mengetahui tentang itu std::aligned_storage, terima kasih! :)
Xeo
Anda juga dapat menggunakan penempatan baru untuk ini.
rustyx
2
@RustyX: Sebenarnya, Anda harus . std::aligned_storage<...>::typehanyalah penyangga mentah yang, tidak seperti char [sizeof(T)], selaras sesuai. Dengan sendirinya, itu lembam: tidak menginisialisasi ingatannya, tidak membangun objek, tidak ada. Oleh karena itu, setelah Anda memiliki buffer jenis ini, Anda harus membuat objek secara manual di dalamnya (dengan metode penempatan newatau pengalokasi construct) dan Anda juga harus merusak objek di dalamnya secara manual (baik secara manual menggunakan destruktor mereka atau menggunakan destroymetode alokasi . ).
Matthieu M.
22

Stroustrup, dalam bahasa pemrograman C ++ (edisi ke-4) §25.3 , menyatakan:

Varian dari teknik menggunakan representasi runt-time tunggal untuk nilai-nilai sejumlah jenis dan mengandalkan sistem tipe (statis) untuk memastikan bahwa mereka hanya digunakan sesuai dengan tipe yang mereka nyatakan telah disebut tipe erasure .

Secara khusus, tidak diperlukan penggunaan fungsi virtual atau pointer fungsi untuk melakukan penghapusan tipe jika kita menggunakan templat. Kasing, sudah disebutkan dalam jawaban lain, panggilan destruktor yang benar sesuai dengan jenis yang disimpan dalam a std::shared_ptr<void>adalah contoh dari itu.

Contoh yang diberikan dalam buku Stroustrup sama menyenangkannya.

Pikirkan tentang implementasi template<class T> class Vector, sebuah wadah di sepanjang garis std::vector. Ketika Anda akan menggunakan Anda Vectordengan banyak jenis pointer yang berbeda, seperti yang sering terjadi, kompiler seharusnya akan menghasilkan kode yang berbeda untuk setiap jenis pointer.

Kode ini mengasapi dapat dicegah dengan mendefinisikan spesialisasi Vector untuk void*pointer dan kemudian menggunakan spesialisasi ini sebagai implementasi dasar umum Vector<T*>untuk semua jenis lainnya T:

template<typename T>
class Vector<T*> : private Vector<void*>{
// all the dirty work is done once in the base class only 
public:
    // ...
    // static type system ensures that a reference of right type is returned
    T*& operator[](size_t i) { return reinterpret_cast<T*&>(Vector<void*>::operator[](i)); }
};

Seperti yang Anda lihat, kita memiliki wadah sangat diketik tapi Vector<Animal*>, Vector<Dog*>, Vector<Cat*>, ..., akan berbagi sama (C ++ dan biner) kode untuk implementasi, memiliki tipe pointer mereka terhapus belakang void*.

Paolo M
sumber
2
Tanpa bermaksud menghujat: Saya lebih suka CRTP daripada teknik yang diberikan oleh Stroustrup.
davidhigh
@davidhigh Apa maksudmu?
Paolo M
Seseorang dapat memperoleh perilaku yang sama (dengan sintaks yang kurang akward) dengan menggunakan kelas dasar CRTPtemplate<typename Derived> VectorBase<Derived> yang kemudian dikhususkan sebagai template<typename T> VectorBase<Vector<T*> >. Selain itu, pendekatan ini tidak hanya berfungsi untuk pointer, tetapi untuk jenis apa pun.
davidhigh
3
Perhatikan bahwa penghubung C ++ yang bagus menggabungkan metode dan fungsi yang identik: penghubung emas, atau lipat komdat MSVC. Kode dihasilkan, tetapi kemudian dibuang saat menautkan.
Yakk - Adam Nevraumont
1
@davidhigh Saya mencoba memahami komentar Anda dan bertanya-tanya apakah Anda bisa memberi saya tautan atau nama pola yang akan dicari (bukan CRTP, tetapi nama teknik yang memungkinkan penghapusan tipe tanpa fungsi virtual atau fungsi pointer) . Dengan Hormat, - Chris
Chris Chiasson
7

Seperti yang dinyatakan oleh Marc, seseorang dapat menggunakan pemain std::shared_ptr<void>. Misalnya menyimpan jenis dalam pointer fungsi, melemparkannya dan menyimpannya di functor hanya satu jenis:

#include <iostream>
#include <memory>
#include <functional>

using voidFun = void(*)(std::shared_ptr<void>);

template<typename T>
void fun(std::shared_ptr<T> t)
{
    std::cout << *t << std::endl;
}

int main()
{
    std::function<void(std::shared_ptr<void>)> call;

    call = reinterpret_cast<voidFun>(fun<std::string>);
    call(std::make_shared<std::string>("Hi there!"));

    call = reinterpret_cast<voidFun>(fun<int>);
    call(std::make_shared<int>(33));

    call = reinterpret_cast<voidFun>(fun<char>);
    call(std::make_shared<int>(33));


    // Output:,
    // Hi there!
    // 33
    // !
}
Janek Olszak
sumber