พิมพ์เทคนิคการลบ


136

(ด้วยการลบประเภทฉันหมายถึงการซ่อนข้อมูลประเภทบางส่วนหรือทั้งหมดเกี่ยวกับคลาสเช่นBoost อะไรก็ได้)
ฉันต้องการใช้เทคนิคการลบประเภทในขณะเดียวกันก็แบ่งปันสิ่งเหล่านั้นซึ่งฉันรู้จัก ความหวังของฉันคือการได้พบกับเทคนิคบ้าๆที่ใครบางคนนึกถึงในชั่วโมงที่มืดมนที่สุดของเขา / เธอ :)

แนวทางแรกและชัดเจนที่สุดและใช้กันทั่วไปที่ฉันรู้คือฟังก์ชันเสมือนจริง เพียงซ่อนการนำคลาสของคุณไปใช้ภายในลำดับชั้นคลาสที่ใช้อินเทอร์เฟซ ไลบรารี Boost จำนวนมากทำสิ่งนี้เช่นBoost สิ่งใดก็ตามที่ทำสิ่งนี้เพื่อซ่อนประเภทและBoostของคุณShared_ptrทำสิ่งนี้เพื่อซ่อนกลไกการจัดสรร (de)

จากนั้นมีตัวเลือกที่มีฟังก์ชันพอยน์เตอร์ไปยังฟังก์ชันเทมเพลตในขณะที่ถือวัตถุจริงไว้ในvoid*ตัวชี้เช่นBoost ฟังก์ชันจะซ่อนประเภทจริงของ functor ตัวอย่างการใช้งานสามารถพบได้ในตอนท้ายของคำถาม

ดังนั้นสำหรับคำถามที่แท้จริงของฉัน:
คุณรู้เทคนิคการลบประเภทใดอีกบ้าง หากเป็นไปได้โปรดระบุรหัสตัวอย่างกรณีการใช้งานประสบการณ์ของคุณกับพวกเขาและลิงก์สำหรับการอ่านเพิ่มเติมหากเป็นไปได้

แก้ไข
(เนื่องจากฉันไม่แน่ใจว่าจะเพิ่มสิ่งนี้เป็นคำตอบหรือเพียงแค่แก้ไขคำถามฉันจะทำแบบที่ปลอดภัยกว่านี้)
อีกหนึ่งเทคนิคที่ดีในการซ่อนประเภทที่แท้จริงของบางสิ่งโดยไม่มีฟังก์ชันเสมือนจริงหรือการvoid*เล่นซอคือ GMan คนหนึ่งมีพนักงานที่นี่โดยมีความเกี่ยวข้องกับคำถามของฉันว่ามันทำงานอย่างไร


รหัสตัวอย่าง:

#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();
}

1
โดย "type erasure" คุณหมายถึง "polymorphism" จริงหรือ? ฉันคิดว่า "type erasure" มีความหมายที่ค่อนข้างเฉพาะเจาะจงซึ่งมักจะเกี่ยวข้องกับเช่น Java generics
Oliver Charlesworth

3
@Oli: การลบประเภทสามารถใช้กับความหลากหลายได้ แต่นั่นไม่ใช่ทางเลือกเดียวตัวอย่างที่สองของฉันแสดงให้เห็นว่า :) และด้วยการลบประเภทฉันหมายความว่าโครงสร้างของคุณไม่ได้ขึ้นอยู่กับประเภทเทมเพลตเช่น เพิ่มฟังก์ชันไม่สนใจว่าคุณจะป้อน functor ตัวชี้ฟังก์ชันหรือแม้กระทั่งแลมบ์ดา เช่นเดียวกันกับ Boost.Shared_Ptr คุณสามารถระบุตัวจัดสรรและฟังก์ชันการยกเลิกการจัดสรรได้ แต่ประเภทที่แท้จริงของตัวจัดสรรshared_ptrไม่ได้สะท้อนถึงสิ่งนี้มันจะเหมือนกันเสมอshared_ptr<int>เช่นไม่เหมือนกับคอนเทนเนอร์มาตรฐาน
Xeo

2
@ Matthieu: ฉันคิดว่าตัวอย่างที่สองยังพิมพ์ว่าปลอดภัย คุณจะทราบประเภทที่แน่นอนที่คุณใช้งานอยู่เสมอ หรือฉันขาดอะไรไป?
Xeo

2
@ Matthieu: คุณพูดถูก โดยปกติAsฟังก์ชันดังกล่าวจะไม่ถูกนำมาใช้ด้วยวิธีนี้ อย่างที่บอกว่าไม่ปลอดภัยต่อการใช้งาน! :)
Xeo

4
@lurscher: อืม ... ไม่เคยใช้ boost หรือ std เวอร์ชันใดต่อไปนี้เลยเหรอ? function, shared_ptr, anyฯลฯ ? พวกเขาทั้งหมดใช้การลบประเภทเพื่อความสะดวกของผู้ใช้ที่น่ารัก
Xeo

คำตอบ:


101

เทคนิคการลบประเภททั้งหมดใน C ++ ทำได้ด้วยตัวชี้ฟังก์ชัน (สำหรับพฤติกรรม) และvoid*(สำหรับข้อมูล) วิธีการ "ต่างกัน" นั้นแตกต่างกันเพียงแค่วิธีเติมน้ำตาลเชิงความหมาย ฟังก์ชันเสมือนเช่นเป็นเพียงน้ำตาลเชิงความหมายสำหรับ

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

iow: ตัวชี้ฟังก์ชัน

ที่กล่าวว่ามีเทคนิคหนึ่งที่ฉันชอบเป็นพิเศษ: เป็นshared_ptr<void>เพียงเพราะมันทำให้คนที่ไม่รู้จักคุณสามารถทำได้: คุณสามารถจัดเก็บข้อมูลใด ๆ ใน a shared_ptr<void>และยังมีตัวทำลายที่ถูกต้องที่เรียกว่า end เนื่องจากตัวshared_ptrสร้างเป็นเทมเพลตฟังก์ชันและจะใช้ประเภทของวัตถุจริงที่ส่งผ่านสำหรับการสร้าง deleter โดยค่าเริ่มต้น:

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

แน่นอนว่านี่เป็นเพียงการvoid*ลบประเภทตัวชี้ / ฟังก์ชันปกติแต่บรรจุได้สะดวกมาก


9
บังเอิญฉันต้องอธิบายพฤติกรรมของshared_ptr<void>เพื่อนของฉันด้วยการใช้งานตัวอย่างเมื่อไม่กี่วันที่ผ่านมา :) มันเจ๋งมาก
Xeo

คำตอบที่ดี; เพื่อให้มันน่าทึ่งภาพร่างของวิธีการสร้าง vtable ปลอมสำหรับแต่ละประเภทที่ถูกลบนั้นน่าศึกษามาก โปรดทราบว่าการใช้ vtables ปลอมและฟังก์ชันพอยน์เตอร์ทำให้คุณทราบโครงสร้างขนาดหน่วยความจำ (เมื่อเทียบกับประเภทเสมือนจริง) ซึ่งสามารถจัดเก็บได้ง่ายในเครื่องและ (อย่างง่ายดาย) หย่าร้างจากข้อมูลที่พวกเขากำลังจำลองเสมือน
Yakk - Adam Nevraumont

ดังนั้นหาก shared_ptr เก็บ Derived * ไว้แล้ว แต่ Base * ไม่ได้ประกาศว่า destructor เป็นเสมือน shared_ptr <void> ยังคงทำงานตามที่ตั้งใจไว้เนื่องจากไม่เคยรู้เกี่ยวกับคลาสพื้นฐานที่จะเริ่มต้นด้วยซ้ำ เย็น!
TamaMcGlinn

@Apollys: ทำ แต่unique_ptrไม่ได้พิมพ์ - ลบตัวลบดังนั้นหากคุณต้องการกำหนด a unique_ptr<T>ให้กับ a unique_ptr<void>คุณต้องระบุอาร์กิวเมนต์ deleter อย่างชัดเจนซึ่งรู้วิธีการลบTผ่านไฟล์void*. ถ้าตอนนี้คุณต้องการที่จะกำหนดSเกินไปแล้วคุณจะต้อง Deleter ที่ชัดเจนที่รู้วิธีการลบTผ่านvoid*และยังSผ่านvoid*, และ , รับvoid*, รู้ไม่ว่าจะเป็นหรือT Sเมื่อถึงจุดนั้นคุณได้เขียนตัวลบประเภทที่ถูกลบไปunique_ptrแล้วและมันก็ใช้ได้unique_ptrเช่นกัน แค่ไม่ออกนอกกรอบ
Marc Mutz - mmutz

ฉันรู้สึกว่าคำถามที่คุณตอบคือ "ฉันจะแก้ไขปัญหานี้ได้unique_ptrอย่างไร" มีประโยชน์สำหรับบางคน แต่ไม่ได้ตอบคำถามของฉัน ฉันเดาว่าคำตอบคือเนื่องจากตัวชี้ที่ใช้ร่วมกันได้รับความสนใจมากขึ้นในการพัฒนาไลบรารีมาตรฐาน ซึ่งฉันคิดว่าเป็นเรื่องน่าเศร้าเล็กน้อยเพราะคำแนะนำที่ไม่ซ้ำใครนั้นง่ายกว่าดังนั้นจึงควรใช้ฟังก์ชันพื้นฐานได้ง่ายกว่าและมีประสิทธิภาพมากกว่าดังนั้นผู้คนจึงควรใช้มันมากขึ้น แต่เรากลับตรงกันข้าม
Apollys สนับสนุน Monica

54

โดยพื้นฐานแล้วสิ่งเหล่านี้คือตัวเลือกของคุณ: ฟังก์ชันเสมือนจริงหรือตัวชี้ฟังก์ชัน

วิธีจัดเก็บข้อมูลและเชื่อมโยงกับฟังก์ชันอาจแตกต่างกันไป ตัวอย่างเช่นคุณสามารถจัดเก็บตัวชี้ไปยังฐานและให้คลาสที่ได้รับมามีข้อมูลและการใช้งานฟังก์ชันเสมือนหรือคุณสามารถจัดเก็บข้อมูลไว้ที่อื่น (เช่นในบัฟเฟอร์ที่จัดสรรแยกต่างหาก) และมีเพียงคลาสที่ได้รับมาให้ การใช้งานฟังก์ชันเสมือนซึ่งใช้เวลาvoid*ที่ชี้ไปที่ข้อมูล หากคุณเก็บข้อมูลไว้ในบัฟเฟอร์แยกกันคุณสามารถใช้ตัวชี้ฟังก์ชันแทนฟังก์ชันเสมือนได้

การจัดเก็บตัวชี้ไปยังฐานจะทำงานได้ดีในบริบทนี้แม้ว่าข้อมูลจะถูกจัดเก็บแยกกันหากมีการดำเนินการหลายอย่างที่คุณต้องการใช้กับข้อมูลที่ลบประเภทของคุณ มิฉะนั้นคุณจะจบลงด้วยตัวชี้ฟังก์ชันหลายตัว (หนึ่งตัวสำหรับฟังก์ชันที่ถูกลบแต่ละประเภท) หรือฟังก์ชันที่มีพารามิเตอร์ที่ระบุการดำเนินการที่จะดำเนินการ


1
ดังนั้นกล่าวอีกนัยหนึ่งคือตัวอย่างที่ฉันให้ไว้ในคำถาม? แม้ว่าจะต้องขอขอบคุณที่เขียนแบบนี้โดยเฉพาะอย่างยิ่งเขียนถึงฟังก์ชันเสมือนจริงและการดำเนินการหลายอย่างกับข้อมูลที่ลบประเภท
Xeo

มีตัวเลือกอื่นอย่างน้อย 2 ตัวเลือก ฉันกำลังเขียนคำตอบ
John Dibling

25

ฉันยังจะพิจารณา (คล้ายกับvoid*) การใช้ "การจัดเก็บข้อมูลดิบ" char buffer[N]นี้:

ใน C ++ 0x คุณมีstd::aligned_storage<Size,Align>::typeสำหรับสิ่งนี้

คุณสามารถจัดเก็บอะไรก็ได้ที่คุณต้องการในนั้นตราบเท่าที่มีขนาดเล็กพอและคุณจัดการกับการจัดตำแหน่งได้อย่างเหมาะสม


4
ใช่แล้ว Boost.Function ใช้การรวมกันของสิ่งนี้กับตัวอย่างที่สองที่ฉันให้ไว้ ถ้า functor มีขนาดเล็กพอมันจะเก็บไว้ภายใน functor_buffer สิ่งที่ควรทราบstd::aligned_storageขอบคุณ! :)
Xeo

คุณยังสามารถใช้ตำแหน่งใหม่สำหรับสิ่งนี้ได้
rustyx

2
@RustyX: ที่จริงแล้วคุณมีการ std::aligned_storage<...>::typeเป็นเพียงบัฟเฟอร์ดิบซึ่งแตกต่างจากการchar [sizeof(T)]จัดแนวอย่างเหมาะสม แม้ว่าตัวมันเองจะเฉื่อย: มันไม่ได้เริ่มต้นหน่วยความจำไม่สร้างวัตถุไม่มีอะไรเลย ดังนั้นเมื่อคุณมีบัฟเฟอร์ประเภทนี้แล้วคุณจะต้องสร้างอ็อบเจกต์ภายในด้วยตนเอง (โดยใช้ตำแหน่งnewหรือconstructวิธีการจัดสรร) และคุณต้องทำลายอ็อบเจ็กต์ที่อยู่ภายในด้วยตนเองด้วย (ไม่ว่าจะเรียกตัวทำลายด้วยตนเองหรือใช้destroyวิธีการจัดสรร).
Matthieu M.

22

Stroustrup ในภาษาโปรแกรม C ++ (รุ่นที่ 4) §25.3สถานะ:

สายพันธุ์ของเทคนิคในการใช้เป็นตัวแทนแคระเวลาเดียวสำหรับค่าของจำนวนชนิดและอาศัย (คงที่) ระบบการพิมพ์เพื่อให้แน่ใจว่าพวกเขาจะใช้เฉพาะตามประเภทประกาศของพวกเขาได้รับการเรียกว่าลบออกประเภท

โดยเฉพาะอย่างยิ่งไม่จำเป็นต้องใช้ฟังก์ชันเสมือนหรือตัวชี้ฟังก์ชันเพื่อทำการลบประเภทหากเราใช้เทมเพลต กรณีที่กล่าวถึงแล้วในคำตอบอื่น ๆ ของการเรียกตัวทำลายล้างที่ถูกต้องตามประเภทที่เก็บไว้ใน a std::shared_ptr<void>เป็นตัวอย่างของสิ่งนั้น

ตัวอย่างที่ให้ไว้ในหนังสือของ Stroustrup ก็น่าสนุกเช่นกัน

คิดเกี่ยวกับการดำเนินการtemplate<class T> class Vector, std::vectorภาชนะตามสายของ เมื่อคุณจะใช้พVectorอยน์เตอร์ประเภทต่างๆมากมายซึ่งมันมักจะเกิดขึ้นคอมไพลเลอร์จะสร้างโค้ดที่แตกต่างกันสำหรับตัวชี้ทุกประเภท

การขยายโค้ดนี้สามารถป้องกันได้โดยกำหนดความเชี่ยวชาญของVectorสำหรับพvoid*อยน์เตอร์จากนั้นใช้ความเชี่ยวชาญนี้เป็นการใช้งานพื้นฐานทั่วไปVector<T*>สำหรับประเภทอื่น ๆ ทั้งหมด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)); }
};

ที่คุณสามารถดูเรามีภาชนะพิมพ์ขอ แต่Vector<Animal*>, Vector<Dog*>, Vector<Cat*>, ... , จะร่วมกัน (C ++ และไบนารี) สำหรับการดำเนินการมีประเภทของพวกเขาชี้ลบvoid*อยู่เบื้องหลัง


2
ไม่มีความหมายที่จะดูหมิ่น: ฉันต้องการ CRTP กับเทคนิคที่ Stroustrup มอบให้
davidhigh

@davidhigh คุณหมายถึงอะไร?
Paolo M

หนึ่งสามารถรับพฤติกรรมเดียวกัน (กับไวยากรณ์ akward น้อยกว่า) โดยใช้CRTPชั้นฐานซึ่งเป็นผู้เชี่ยวชาญแล้วtemplate<typename Derived> VectorBase<Derived> template<typename T> VectorBase<Vector<T*> >ยิ่งไปกว่านั้นวิธีนี้ไม่ได้ใช้ได้เฉพาะกับตัวชี้ แต่สำหรับประเภทใด
davidhigh

3
โปรดทราบว่าตัวเชื่อม C ++ ที่ดีจะรวมวิธีการและฟังก์ชันที่เหมือนกัน: ตัวเชื่อมโยงทองคำหรือการพับคำสั่ง MSVC โค้ดถูกสร้างขึ้น แต่จะถูกทิ้งระหว่างการเชื่อมโยง
Yakk - Adam Nevraumont

1
@davidhigh ฉันพยายามทำความเข้าใจความคิดเห็นของคุณและสงสัยว่าคุณสามารถให้ลิงค์หรือชื่อของรูปแบบที่จะค้นหาให้ฉันได้หรือไม่ (ไม่ใช่ CRTP แต่เป็นชื่อของเทคนิคที่อนุญาตให้ลบประเภทโดยไม่ใช้ฟังก์ชันเสมือนหรือตัวชี้ฟังก์ชัน) . ด้วยความเคารพ - Chris
Chris Chiasson

19

ดูชุดของบทความนี้สำหรับรายการ (ที่ค่อนข้างสั้น) เทคนิคการลบชนิดและการอภิปรายเกี่ยวกับการค้าย์ออฟ: Part I , Part II , Part III , Part IV

สิ่งที่ฉันยังไม่ได้พูดถึงคือAdobe.PolyและBoost.Variantซึ่งถือได้ว่าเป็นการลบประเภทในระดับหนึ่ง


7

ตามที่ Marc ระบุไว้สามารถใช้ร่ายstd::shared_ptr<void>ได้ ตัวอย่างเช่นจัดเก็บประเภทในตัวชี้ฟังก์ชันให้แคสต์และเก็บไว้ใน functor เพียงประเภทเดียว:

#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
    // !
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.