유형 삭제 기술


136

(유형 삭제를 사용하면 Boost.Any 와 같은 클래스에 대한 유형 정보의 일부 또는 전부를 숨기는 것을 의미합니다 .)
유형 삭제 기술을 유지하면서 알고있는 기술을 공유하고 싶습니다. 누군가가 자신의 가장 어두운 시간에 생각했던 미친 기술을 찾기를 바랍니다. :)

내가 아는 첫 번째이자 가장 명백하고 일반적으로 사용되는 접근 방식은 가상 기능입니다. 인터페이스 기반 클래스 계층 구조에서 클래스 구현을 숨기십시오. 많은 Boost 라이브러리가 이것을 수행합니다 (예 : Boost.Any 는 유형을 숨기고 이를 수행 하며 Boost.Shared_ptr 은 (de) 할당 메커니즘을 숨기려고합니다).

그런 다음 템플릿과 같은 함수 포인터가있는 옵션이 있으며 Boostvoid* 와 같은 포인터에 실제 객체를 보유하고 함수 는 실제 유형의 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"는 다소 특정한 의미를 지니고 있는데, 이는 일반적으로 Java 제네릭과 관련이 있습니다.
Oliver Charlesworth

3
@Oli : 타입 삭제는 다형성으로 구현 될 수 있지만, 이것이 유일한 옵션은 아닙니다. 제 두 번째 예는 그 점을 보여줍니다. :) 그리고 유형 지우기를 사용하면 구조체가 예를 들어 템플릿 유형에 의존하지 않는다는 것을 의미합니다. Boost.Function은 functor, 함수 포인터 또는 람다에게 먹이를 주더라도 신경 쓰지 않습니다. Boost.Shared_Ptr과 동일합니다. 할당 자 및 할당 해제 기능을 지정할 수 있지만 실제 유형은 shared_ptr이를 반영하지 않으며 shared_ptr<int>표준 컨테이너와 달리 항상 동일 합니다.
Xeo

2
@ Matthieu : 두 번째 예제도 유형 안전하다고 생각합니다. 당신은 항상 당신이 작동하는 정확한 유형을 알고 있습니다. 아니면 뭔가 빠졌습니까?
Xeo

2
@ Matthieu : 당신이 맞아요. 일반적으로 이러한 As(들) 함수는 그런 식으로 구현되지 않습니다. 내가 말했듯이, 결코 안전한 것은 아닙니다! :)
Xeo

4
@lurscher : 글쎄 ... 다음 부스트 또는 표준 버전을 사용하지 않았 습니까? function, shared_ptr, any, 등? 그들은 모두 달콤한 달콤한 사용자 편의를 위해 유형 삭제를 사용합니다.
Xeo

답변:


100

C ++의 모든 유형 삭제 기술은 함수 포인터 (행동) 및 void*(데이터)로 수행됩니다. "다른"방법은 시맨틱 설탕을 첨가하는 방식이 단순히 다릅니다. 가상 함수, 예를 들어 의미 적 설탕

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

iow : 함수 포인터.

그러나 내가 특히 좋아하는 기술이 하나 있습니다. 그것은 shared_ptr<void>단순히 당신이 이것을 할 수 있다는 것을 모르는 사람들의 마음을 날려 버렸기 때문입니다 shared_ptr<void>. shared_ptr생성자는 함수 템플릿이고 기본적으로 삭제기를 생성하기 위해 전달 된 실제 객체의 유형을 사용 하기 때문에 end :

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

물론 이것은 일반적인 void*/ function-pointer 유형 삭제이지만 매우 편리하게 패키지됩니다.


9
우연히도, 나는 shared_ptr<void>며칠 전에 예제 구현으로 내 친구에게 행동을 설명해야했습니다 . :) 정말 멋지다.
Xeo

좋은 답변; 놀랍도록, 각 지워진 유형에 대해 가짜 가상 테이블을 정적으로 만드는 방법에 대한 스케치는 매우 교육적입니다. 가짜 vtables 및 함수 포인터 구현은 로컬에 쉽게 저장하고 가상화 할 데이터와 쉽게 분리 할 수있는 알려진 메모리 크기 구조 (순수 가상 유형과 비교)를 제공합니다.
Yakk-Adam Nevraumont

따라서 shared_ptr이 Derived *를 저장하지만 Base *가 소멸자를 virtual로 선언하지 않은 경우 shared_ptr <void>는 기본 클래스에 대해 전혀 알지 못했기 때문에 의도 한대로 작동합니다. 멋있는!
TamaMcGlinn

@Apollys : unique_ptr지우개를 입력 하지만 지우지 않습니다. 따라서에 a unique_ptr<T>를 지정 unique_ptr<void>하려면를 T통해 삭제하는 방법을 알고있는 deleter 인수를 명시 적으로 제공해야 합니다 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

25

또한 void*"raw storage"의 사용을 고려할 것입니다 : 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
신성 모독이라는 의미는 없습니다 : 저는 Stroustrup이 제공 한 기술보다 CRTP를 선호합니다.
davidhigh

@davidhigh 무슨 뜻인가요?
Paolo M

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 2016 년


7

Marc가 말했듯이 cast 사용할 수 있습니다 std::shared_ptr<void>. 예를 들어 유형 을 함수 포인터에 저장하고 캐스트하고 한 유형의 펑터에 저장하십시오.

#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.