简体   繁体   中英

Call a derived class' (non-virtual) function in the base class' Destructor

Suppose we have the following class template:

template<typename T>
class Object
{
public:
    Object() = default;
    Object(const Object&) = delete;
    Object(Object&& other) noexcept
    {
        if (this != &other)
        {
            static_cast<T*>(this)->Release();
            m_Id = std::exchange(other.m_Id, 0);
        }
    }

    auto operator=(const Object&) = delete;
    Object& operator=(Object&& other) noexcept
    {
        if (this != &other) {
            static_cast<T*>(this)->Release();
            m_Id = std::exchange(other.m_Id, 0);
        }

        return *this;
    }

    ~Object()
    {
        static_cast<T*>(this)->Release();
        m_Id = 0;
    }

protected:
    std::uint32_t m_Id;
};

(Please ignore the duplication in the move constructor and move assignment operator for the moment)

This class is meant to act as a base class for OpenGL object wrappers. Following is an example use:

class VertexBuffer : public Object<VertexBuffer>
{
public:
    VertexBuffer()
    {
        glGenBuffers(1, &m_Id);
        ...
    }

    void Release()
    {
        glDeleteBuffers(1, &m_Id);
    }
};

The Object<T> class template is supposed to take care of the bookkeeping.

The reason for doing this is that the pattern in the Object<T> class is repeated the exact same way for (almost) every OpenGL object wrapper that might be written. The only difference is the creation and deletion of the objects which is handled by the constructor and the Release() function in this example.

Now the question is whether this ( Object<T>::~Object() to be specific) taps into UB land? Undefined Behavior Sanitizer doesn't report any errors but I've never done this, so I though of asking people with more experience to make sure.

Don't do that. That'll cause an undefined behavior.

Instead, implement the template class as a derived class, like the following example.

class BufferGrandBase {
protected:
    GLuint id;
};

template<class B>
class Buffer : public B {
public:
    Buffer() {
        B::Create();
    }
    ~Buffer() {
        B::Destroy();
    }
};

class VertexBufferBase : public BufferGrandBase {
public:
    void Create() { glGenBuffers(1, &id); }
    void Destroy() { glDeleteBuffers(1, &id); }
};
typedef Buffer<VertexBufferBase> VertexBuffer;

This pattern will also simplify implementing constructors and operators.

Short answer: Yes, this is undefined behaviour , don't do that.

Long answer:
The destruction of VertexBuffer invokes first ~VertexBuffer() and then invokes ~Object<VertexBuffer>() afterwards. When ~Object<VertexBuffer>() is invoked the VertexBuffer "part" of the object has already been destroyed, ie you are now doing an illegal downcast via static_cast (the remaining valid part of the object is a Object<VertexBuffer> , not a VertexBuffer ).

And undefined behaviour permits the compiler to do ANYTHING - it might even (appear to) work, only to suddenly stop working (or only work when you build in Debug mode, but not when in Release). So, for your own sake, please don't do that.

This is close to the model of std::unique_ptr<> with a custom deleter. But it doesn't quite fit because std::unique_ptr has to hold a pointer and this case needs an integer handle-type.

So here is a brief example. It is general good advice to prefer inclusion over inheritance so I have deliberately placed the helper object inside the owning class.

All I'm really trying to demonstrate here is that there are a number of ways to customise behaviours in templates beyond call members of instances.

#include <iostream>
#include <memory>

typedef size_t gd_size;
typedef int gd_handle;

void gen_buffers(gd_size size,gd_handle*buffs);
void del_buffers(gd_size size,gd_handle*buffs);

typedef void (*gd_func)(gd_size,gd_handle*buff);

template<gd_func gfunc,gd_func dfunc> class GenDel{
    public:
        GenDel(){gfunc(1,&buff);}
        ~GenDel(){dfunc(1,&buff);}
        int get()const{return buff;}
    private:
    int buff;
    
    GenDel(const GenDel&)=delete;
};

class BufferHolder{
public:      

   BufferHolder(){}
   
   void do_thing() const{
       std::cout<<"using "<<buffer.get()<<'\n';
   }
   
 private:
    GenDel<gen_buffers,del_buffers> buffer;
};

int main() {
    BufferHolder b;
    BufferHolder c;

    b.do_thing();
    
    return 0;
}

int seed{0};

void gen_buffers(gd_size size,gd_handle*buffs){
    for(size_t i{0};i<size;++i){
        buffs[i]=++seed;
        std::cout << "generated "<< buffs[i] << '\n';
    }
}

void del_buffers(gd_size size,gd_handle*buffs){
        for(gd_size i{0};i<size;++i){
        std::cout << "deleted "<< buffs[i] << '\n';
    }
}

If you have a "thing" that holds an Object<T> where T is the Crtp -type, that "thing" is likely a template anyway.

So instead of holding an Object<T> , why don't you just hold a T , which is the full type that inherits from Object<T> . If that is destroyed it will call T::~T() automatically.

In addition, perhaps you want to do private or protected inheritance from Object<T> to discourage users from slicing the Crtp-type.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM