Loại kỹ thuật tẩy


136

(Với kiểu xóa, ý tôi là ẩn một số hoặc tất cả thông tin loại liên quan đến một lớp, hơi giống như Boost.Any .)
Tôi muốn nắm giữ các kỹ thuật xóa kiểu, đồng thời chia sẻ những thông tin mà tôi biết. Hy vọng của tôi là tìm thấy một số kỹ thuật điên rồ mà ai đó nghĩ đến trong giờ đen tối nhất của anh ấy / cô ấy. :)

Cách tiếp cận đầu tiên và rõ ràng nhất và thường được thực hiện, mà tôi biết, là các hàm ảo. Chỉ cần ẩn việc thực hiện lớp của bạn bên trong một hệ thống phân cấp lớp dựa trên giao diện. Nhiều thư viện Boost làm điều này, ví dụ Boost.Any làm điều này để ẩn loại của bạn và Boost.Shared_ptr làm điều này để ẩn cơ chế phân bổ (de).

Sau đó, có tùy chọn với các con trỏ hàm cho các hàm templated, trong khi giữ đối tượng thực tế trong một void*con trỏ, giống như Boost.Factor không để ẩn kiểu thực của hàm functor. Ví dụ triển khai có thể được tìm thấy ở cuối câu hỏi.

Vì vậy, đối với câu hỏi thực tế của tôi:
Những kỹ thuật tẩy xóa loại nào khác mà bạn biết? Vui lòng cung cấp cho họ, nếu có thể, với một mã ví dụ, các trường hợp sử dụng, kinh nghiệm của bạn với họ và có thể liên kết để đọc thêm.

Chỉnh sửa
(Vì tôi không chắc chắn sẽ thêm câu này dưới dạng câu trả lời hoặc chỉ chỉnh sửa câu hỏi, tôi sẽ chỉ thực hiện một cách an toàn hơn.)
Một kỹ thuật hay khác để ẩn loại thực tế của một thứ không có chức năng ảo hoặc void*nghịch ngợm, là một GMan sử dụng ở đây , liên quan đến câu hỏi của tôi về cách chính xác điều này hoạt động.


Mã ví dụ:

#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
Theo "kiểu xóa", bạn có thực sự đề cập đến "đa hình" không? Tôi nghĩ rằng "kiểu xóa" có một ý nghĩa cụ thể, thường được liên kết với ví dụ chung về Java.
Oliver Charlesworth

3
@Oli: Loại tẩy có thể được thực hiện với đa hình, nhưng đó không phải là lựa chọn duy nhất, ví dụ thứ hai của tôi cho thấy điều đó. :) Và với kiểu xóa tôi chỉ có nghĩa là, cấu trúc của bạn không phụ thuộc vào một kiểu mẫu chẳng hạn. Boost.Function không quan tâm nếu bạn cho nó ăn functor, con trỏ hàm hoặc thậm chí là lambda. Tương tự với Boost.Shared_Ptr. Bạn có thể chỉ định một hàm phân bổ và chức năng phân bổ, nhưng loại thực tế shared_ptrkhông phản ánh điều này, nó sẽ luôn giống nhau, shared_ptr<int>ví dụ, không giống như container tiêu chuẩn.
Xèo

2
@Matthieu: Tôi xem xét ví dụ thứ hai cũng gõ an toàn. Bạn luôn biết chính xác loại bạn đang hoạt động. Hay tôi đang thiếu một cái gì đó?
Xèo

2
@Matthieu: Bạn nói đúng. Thông thường Aschức năng (s) như vậy sẽ không được thực hiện theo cách đó. Như tôi đã nói, không có nghĩa là an toàn để sử dụng! :)
Xèo

4
@lurscher: Chà ... không bao giờ sử dụng các phiên bản boost hoặc std của bất kỳ điều nào sau đây? function, shared_ptr, any, Vv? Tất cả đều sử dụng loại tẩy xóa để thuận tiện cho người dùng ngọt ngào.
Xèo

Câu trả lời:


100

Tất cả các kỹ thuật xóa kiểu trong C ++ được thực hiện với các con trỏ hàm (đối với hành vi) và void*(đối với dữ liệu). Các phương pháp "khác nhau" chỉ đơn giản là khác nhau về cách chúng thêm đường ngữ nghĩa. Các hàm ảo, ví dụ, chỉ là đường ngữ nghĩa cho

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

iow: con trỏ hàm.

Điều đó nói rằng, có một kỹ thuật tôi đặc biệt thích, mặc dù: Đó là shared_ptr<void>, đơn giản là vì nó thổi bay tâm trí của những người không biết bạn có thể làm điều này: Bạn có thể lưu trữ bất kỳ dữ liệu nào trong một shared_ptr<void>, và vẫn có hàm hủy chính xác được gọi tại kết thúc, bởi vì hàm shared_ptrtạo là một mẫu hàm và sẽ sử dụng loại đối tượng thực tế được truyền để tạo deleter theo mặc định:

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

Tất nhiên, đây chỉ là void*kiểu xóa con trỏ / hàm con trỏ thông thường , nhưng được đóng gói rất thuận tiện.


9
Thật trùng hợp, tôi đã phải giải thích hành vi của shared_ptr<void>một người bạn của tôi với một ví dụ thực hiện chỉ vài ngày trước. :) Nó thực sự là mát mẻ.
Xèo

Câu trả lời tốt; để làm cho nó tuyệt vời, một bản phác thảo về cách một vtable giả có thể được tạo tĩnh cho mỗi loại bị xóa là rất giáo dục. Lưu ý rằng việc triển khai fake-vtables và con trỏ hàm cung cấp cho bạn các cấu trúc có kích thước bộ nhớ đã biết (so với các loại ảo thuần) có thể dễ dàng lưu trữ cục bộ và (dễ dàng) tách khỏi dữ liệu mà chúng đang ảo hóa.
Yakk - Adam Nevraumont

vì vậy, nếu shared_ptr sau đó lưu trữ Derogen *, nhưng Base * không khai báo hàm hủy là ảo, shared_ptr <void> vẫn hoạt động như dự định, vì nó thậm chí không bao giờ biết về một lớp cơ sở để bắt đầu. Mát mẻ!
TamaMcGlinn

@Apollys: Nó không, nhưng unique_ptrkhông xóa loại deleter, vì vậy nếu bạn muốn gán a unique_ptr<T>cho a unique_ptr<void>, bạn cần cung cấp một đối số deleter, rõ ràng, biết cách xóa Tthông qua a void*. Nếu bây giờ bạn cũng muốn gán một cái S, thì bạn cần một deleter, rõ ràng, biết cách xóa a Tqua void*và cũng Sthông qua a void*, , đưa ra void*, biết rằng đó là một Thay một S. Tại thời điểm đó, bạn đã viết một deleter xóa loại unique_ptr, và sau đó nó cũng hoạt động cho unique_ptr. Chỉ cần không ra khỏi hộp.
Marc Mutz - mmutz

Tôi cảm thấy như câu hỏi bạn trả lời là "Làm thế nào để tôi giải quyết được thực tế là điều này không hiệu quả unique_ptr?" Hữu ích cho một số người, nhưng không giải quyết câu hỏi của tôi. Tôi đoán câu trả lời là bởi vì các con trỏ chia sẻ đã thu hút sự chú ý nhiều hơn trong việc phát triển thư viện chuẩn. Điều tôi nghĩ là hơi buồn vì các con trỏ độc đáo đơn giản hơn, do đó sẽ dễ thực hiện các chức năng cơ bản hơn và chúng hiệu quả hơn vì vậy mọi người nên sử dụng chúng nhiều hơn. Thay vào đó chúng ta có điều hoàn toàn ngược lại.
Apollys hỗ trợ Monica

54

Về cơ bản, đó là những lựa chọn của bạn: hàm ảo hoặc con trỏ hàm.

Cách bạn lưu trữ dữ liệu và liên kết nó với các chức năng có thể khác nhau. Ví dụ: bạn có thể lưu trữ một con trỏ đến cơ sở và có lớp dẫn xuất chứa dữ liệu các triển khai chức năng ảo hoặc bạn có thể lưu trữ dữ liệu ở nơi khác (ví dụ: trong một bộ đệm được phân bổ riêng) và chỉ có lớp dẫn xuất cung cấp việc thực hiện chức năng ảo, lấy void*điểm đó để dữ liệu. Nếu bạn lưu trữ dữ liệu trong một bộ đệm riêng biệt, thì bạn có thể sử dụng các con trỏ hàm thay vì các hàm ảo.

Lưu trữ một con trỏ đến cơ sở hoạt động tốt trong ngữ cảnh này, ngay cả khi dữ liệu được lưu trữ riêng, nếu có nhiều thao tác bạn muốn áp dụng cho dữ liệu bị xóa loại của mình. Mặt khác, bạn kết thúc với nhiều con trỏ hàm (một cho mỗi hàm bị xóa loại) hoặc các hàm có tham số chỉ định thao tác để thực hiện.


1
Vì vậy, nói cách khác các ví dụ tôi đã đưa ra trong câu hỏi? Mặc dù, cảm ơn vì đã viết nó lên như thế này, đặc biệt là viết vào các hàm ảo và nhiều thao tác trên dữ liệu bị xóa.
Xèo

Có ít nhất 2 lựa chọn khác. Tôi đang soạn một câu trả lời.
John Dibling

25

Tôi cũng sẽ xem xét (tương tự void*) việc sử dụng "lưu trữ thô" : char buffer[N].

Trong C ++ 0x bạn có std::aligned_storage<Size,Align>::typecho điều này.

Bạn có thể lưu trữ bất cứ thứ gì bạn muốn trong đó, miễn là nó đủ nhỏ và bạn xử lý căn chỉnh đúng cách.


4
Vâng vâng, Boost.Function thực sự sử dụng kết hợp điều này và ví dụ thứ hai tôi đã đưa ra. Nếu functor đủ nhỏ, nó sẽ lưu nó bên trong functor_buffer. Tốt để biết về std::aligned_storagemặc dù, cảm ơn! :)
Xèo

Bạn cũng có thể sử dụng vị trí mới cho việc này.
rustyx

2
@RustyX: Thật ra, bạn phải như vậy. std::aligned_storage<...>::typechỉ là một bộ đệm thô, không giống như char [sizeof(T)], được căn chỉnh phù hợp. Mặc dù vậy, bản thân nó trơ ra: nó không khởi tạo bộ nhớ của nó, không xây dựng một đối tượng, không có gì. Do đó, khi bạn có bộ đệm loại này, bạn phải xây dựng thủ công các đối tượng bên trong nó (với phương thức vị trí newhoặc constructphương thức cấp phát ) và bạn cũng phải hủy thủ công các đối tượng bên trong nó (bằng cách gọi thủ công hàm hủy của chúng hoặc sử dụng destroyphương thức cấp phát ).
Matthieu M.

22

Stroustrup, trong ngôn ngữ lập trình C ++ (ấn bản thứ 4) §25.3 , nêu rõ:

Các biến thể của kỹ thuật sử dụng biểu diễn thời gian chạy đơn cho các giá trị của một số loại và dựa vào hệ thống loại (tĩnh) để đảm bảo rằng chúng chỉ được sử dụng theo loại khai báo của chúng được gọi là loại xóa .

Cụ thể, không cần sử dụng các hàm ảo hoặc con trỏ hàm để thực hiện xóa kiểu nếu chúng ta sử dụng các mẫu. Trường hợp, đã được đề cập trong các câu trả lời khác, của lệnh gọi hàm hủy chính xác theo loại được lưu trong a std::shared_ptr<void>là một ví dụ về điều đó.

Ví dụ được cung cấp trong cuốn sách của Stroustrup cũng thú vị không kém.

Hãy suy nghĩ về việc thực hiện template<class T> class Vector, một container dọc theo dòng std::vector. Khi bạn sẽ sử dụng của bạn Vectorvới rất nhiều loại con trỏ khác nhau, như thường xảy ra, trình biên dịch sẽ tạo ra các mã khác nhau cho mỗi loại con trỏ.

Sự phình to mã này có thể được ngăn chặn bằng cách xác định chuyên môn hóa Vector cho void*con trỏ và sau đó sử dụng chuyên môn này làm triển khai cơ sở chung Vector<T*>cho tất cả các loại khác 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)); }
};

Như bạn có thể thấy, chúng ta có một container gõ mạnh mẽ nhưng Vector<Animal*>, Vector<Dog*>, Vector<Cat*>, ..., sẽ chia sẻ cùng (C ++ nhị phân) mã cho việc thực hiện, có kiểu con trỏ của họ bị xóa đằng sau void*.


2
Không có nghĩa là báng bổ: Tôi thích CRTP hơn là kỹ thuật do Stroustrup đưa ra.
davidhigh

@davidhigh Ý bạn là gì?
Paolo M

Người ta có thể có được hành vi tương tự (với cú pháp ít biến đổi hơn) bằng cách sử dụng lớp cơ sở CRTPtemplate<typename Derived> VectorBase<Derived> sau đó được chuyên môn hóa như template<typename T> VectorBase<Vector<T*> >. Hơn nữa, cách tiếp cận này không chỉ hoạt động cho con trỏ, mà cho bất kỳ loại nào.
davidhigh

3
Lưu ý rằng các trình liên kết C ++ tốt hợp nhất các phương thức và chức năng giống hệt nhau: trình liên kết vàng hoặc MSVC comdat gấp. Mã được tạo ra, nhưng sau đó bị loại bỏ trong quá trình liên kết.
Yakk - Adam Nevraumont

1
@davidhigh Tôi đang cố gắng hiểu nhận xét của bạn và tự hỏi liệu bạn có thể cho tôi một liên kết hoặc tên của một mẫu để tìm kiếm (không phải CRTP, nhưng tên của một kỹ thuật cho phép xóa không có chức năng ảo hoặc con trỏ hàm) . Trân trọng, - Chris
Chris Chiasson


7

Như tuyên bố của Marc, người ta có thể sử dụng diễn viên std::shared_ptr<void>. Ví dụ: lưu trữ kiểu trong một con trỏ hàm, bỏ nó và lưu trữ trong một hàm functor chỉ có một loại:

#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
    // !
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.