5
\$\begingroup\$

[EDIT: Thanks for your opinions. I think this topic is discussed enough. Since std::auto_ptr is removed from C++17, I decided to drop the idea of inheriting from std::auto_ptr to avoid compatibility problems in the future.]

I know that std::auto_ptr is not a perfect class for automatic memory management because it doesn't support the deletion of arrays. However, I decided to create a better templated class (called owner_ptr) which supports this by inheriting from std::auto_ptr. It seems to be dangerous, but I think I've found a safe way to do this. Some advantages why I thought it was a good idea to inherit:

  • I don't have to reimplement all functions of std::auto_ptr.
  • I can use the std::auto_ptr_ref feature to pass temporary owner_ptr objects by value (and pass ownership to the copy).
  • I know that I won't access owner_ptr objects through base class pointer, so the absence of virtual ~auto_ptr() cannot cause problem.

(Here you can find the implementation of std::auto_ptr.)

I've created some test cases (see below) and the result was promising. I want to use the new class in my own project (just a project for myself). But I'm not sure, if there are any scenarios where it can fail. I would be grateful if you could take a look at my implementation and share your ideas/opinions about it. (E.g. if there is a function which I should override too or if it is a bad practice to do this in C++98/C++03.)

#include <memory>
#include <cstddef>
#include <iostream>

template <typename T>
class owner_ptr : public std::auto_ptr<T>
{
public:
    explicit owner_ptr(T* p = NULL) : std::auto_ptr<T>(p) { std::cout << "owner_ptr<T>(T*) called." << std::endl; }
    owner_ptr(owner_ptr<T>& rhs) : std::auto_ptr<T>(rhs.release()) { std::cout << "owner_ptr<T>(owner_ptr<T>&) called." << std::endl; }
    owner_ptr(std::auto_ptr_ref<T> rhs) : std::auto_ptr<T>(rhs) { std::cout << "owner_ptr<T>(auto_ptr_ref<T>) called." << std::endl; }
};

/* "specialization" for arrays */
template <typename T>
class owner_ptr<T[]> : public std::auto_ptr<T>
{
public:
    explicit owner_ptr(T* p = NULL) : std::auto_ptr<T>(p) { std::cout << "owner_ptr<T[]>(T*) called." << std::endl; }
    owner_ptr(owner_ptr<T[]>& rhs) : std::auto_ptr<T>(rhs.release()) { std::cout << "owner_ptr<T[]>(owner_ptr<T[]>) called." << std::endl; }
    owner_ptr(std::auto_ptr_ref<T> rhs) : std::auto_ptr<T>(rhs) { std::cout << "owner_ptr<T[]>(auto_ptr_ref<T>) called." << std::endl; }

    void reset(T* ptr = NULL)
    {
        std::cout << "owner_ptr<T[]>::reset(T*) called." << std::endl;
        if (std::auto_ptr<T>::get() != ptr)
        {
            this->~owner_ptr();
            std::auto_ptr<T>::reset(ptr);
        }
    }

    ~owner_ptr()
    {
        std::cout << "owner_ptr<T[]>::~owner_ptr() called." << std::endl;
        delete[] std::auto_ptr<T>::release();
    }
};

I tried to test it as well as it was possible (sorry for the long code):

class A
{
public:
    A() { std::cout << "A() called." << std::endl; }
    virtual ~A() { std::cout << "~A() called." << std::endl; }
};

class B : public A
{
public:
    B() { std::cout << "B() called." << std::endl; }
    ~B() { std::cout << "~B() called." << std::endl; }
};

void printTest()
{
    static int test = 0;
    std::cout << std::endl << "Test " << ++test << ":" << std::endl;
}

template <typename T>
void testFunction(owner_ptr<T> param)
{
    std::cout << "testFunction<T>() called." << std::endl;
    owner_ptr<T> op = param;
}

template <typename T>
void testFunction(owner_ptr<T[]> param)
{
    std::cout << "testFunction<T[]>() called." << std::endl;
    owner_ptr<T[]> op = param;
}

int main()
{
    std::cout << "Testing owner_ptr<A>" << std::endl;
    /* Test 1 */
    /* Constructing with NULL-pointer */
    printTest();
    owner_ptr<A>();

    /* Test 2 */
    /* Constructing with valid pointer */
    printTest();
    owner_ptr<A>(new A);

    /* Test 3 */
    /* Testing "copy"-constructor */
    printTest();
    {
        owner_ptr<A> op1(new A);
        owner_ptr<A> op2(op1);
        std::cout << "op1.get() result : " << op1.get() << std::endl;
    }

    /* Test 4 */
    /* Testing owner_ptr<T>::operator= */
    printTest();
    {
        owner_ptr<A> op1(new A);
        owner_ptr<A> op2;
        op2 = op1;
        std::cout << "op1.get() result : " << op1.get() << std::endl;
    }

    /* Test 5 */
    /* Passing owner_ptr<A> object by value */
    printTest();
    {
        owner_ptr<A> op(new A);
        testFunction(op);
    }

    /* Test 6 */
    /* Constructing owner_ptr<A> with a temporary owner_ptr<A> object. */
    /* = passing temporary object by non-const reference */
    printTest();
    {
        owner_ptr<A>(owner_ptr<A>(new A));
    }
    /* Explicit constructor : owner_ptr<A>::owner_ptr<A>(A*) */
    /* Implicit conversion : owner_ptr<A>::operator std::auto_ptr_ref<A>() (inherited from std::auto_ptr<A>) */
    /* (Explicit) constructor : auto_ptr_ref<A>::auto_ptr_ref<A>(A*) */
    /* Implicit constructor : owner_ptr<A>::owner_ptr<A>(std::auto_ptr_ref<A>) */

    /* Test 7 */
    /* Passing a temporary owner_ptr<A> object by value */
    printTest();
    {
        testFunction(owner_ptr<A>(new A));
    }


    std::cout << std::endl << "Testing owner_ptr<A[]>" << std::endl;
    /* Test 8 */
    /* Constructing with NULL-pointer */
    printTest();
    owner_ptr<A[]>();

    /* Test 9 */
    /* Constructing with valid pointer */
    printTest();
    owner_ptr<A[]>(new A[3]);

    /* Test 10 */
    /* Testing "copy"-constructor */
    printTest();
    {
        owner_ptr<A[]> op1(new A[3]);
        owner_ptr<A[]> op2(op1);
        std::cout << "op1.get() result : " << op1.get() << std::endl;
    }

    /* Test 11 */
    /* Testing owner_ptr<A[]>::operator= */
    printTest();
    {
        owner_ptr<A[]> op1(new A[3]);
        owner_ptr<A[]> op2;
        op2 = op1;
        std::cout << "op1.get() result : " << op1.get() << std::endl;
    }

    /* Test 12 */
    /* Passing owner_ptr<A[]> object by value */
    printTest();
    {
        owner_ptr<A[]> op(new A[3]);
        testFunction(op);
    }

    /* Test 13 */
    /* Constructing owner_ptr<A[]> with a temporary owner_ptr<A[]> object */
    /* = passing temporary object by non-const reference */
    printTest();
    {
        owner_ptr<A[]>(owner_ptr<A[]>(new A[3]));
    }

    /* Test 14 */
    /* Passing a temporary owner_ptr<A[]> by value */
    printTest();
    {
        testFunction(owner_ptr<A[]>(new A[3]));
    }

    /* Test 15 */
    /* Testing owner_ptr<A[]>::reset */
    printTest();
    {
        owner_ptr<A[]>(new A[3]).reset(new A[2]);
    }

    /* Test 16 */
    printTest();
    {
        owner_ptr<A> op(new B);
    }

    /* Testing compilation of owner_ptr<A> = owner_ptr<A[]> */
    /* Result: compilation error */
    /*
    {
        owner_ptr<A> op1;
        owner_ptr<A[]> op2(new A[3]);
        op1 = op2;
        Result: compiler error.
    }
    */

    /* std::vector< owner_ptr<A> > container;
       container.push_back(owner_ptr<A>(new B));
       Won't compile due to missing
       owner_ptr<A>(const owner_ptr<A>&) constructor.
       R.I.P. */

    return 0;
}

The result was the following:

Testing owner_ptr<A>

Test 1:
owner_ptr<T>(T*) called.

Test 2:
A() called.
owner_ptr<T>(T*) called.
~A() called.

Test 3:
A() called.
owner_ptr<T>(T*) called.
owner_ptr<T>(owner_ptr<T>&) called.
op1.get() result : 0
~A() called.

Test 4:
A() called.
owner_ptr<T>(T*) called.
owner_ptr<T>(T*) called.
op1.get() result : 0
~A() called.

Test 5:
A() called.
owner_ptr<T>(T*) called.
owner_ptr<T>(owner_ptr<T>&) called.
testFunction<T>() called.
owner_ptr<T>(owner_ptr<T>&) called.
~A() called.

Test 6:
A() called.
owner_ptr<T>(T*) called.
owner_ptr<T>(auto_ptr_ref<T>) called.
~A() called.

Test 7:
A() called.
owner_ptr<T>(T*) called.
owner_ptr<T>(auto_ptr_ref<T>) called.
testFunction<T>() called.
owner_ptr<T>(owner_ptr<T>&) called.
~A() called.

Testing owner_ptr<A[]>

Test 8:
owner_ptr<T[]>(T*) called.
owner_ptr<T[]>::~owner_ptr() called.

Test 9:
A() called.
A() called.
A() called.
owner_ptr<T[]>(T*) called.
owner_ptr<T[]>::~owner_ptr() called.
~A() called.
~A() called.
~A() called.

Test 10:
A() called.
A() called.
A() called.
owner_ptr<T[]>(T*) called.
owner_ptr<T[]>(owner_ptr<T[]>) called.
op1.get() result : 0
owner_ptr<T[]>::~owner_ptr() called.
~A() called.
~A() called.
~A() called.
owner_ptr<T[]>::~owner_ptr() called.

Test 11:
A() called.
A() called.
A() called.
owner_ptr<T[]>(T*) called.
owner_ptr<T[]>(T*) called.
op1.get() result : 0
owner_ptr<T[]>::~owner_ptr() called.
~A() called.
~A() called.
~A() called.
owner_ptr<T[]>::~owner_ptr() called.

Test 12:
A() called.
A() called.
A() called.
owner_ptr<T[]>(T*) called.
owner_ptr<T[]>(owner_ptr<T[]>) called.
testFunction<T[]>() called.
owner_ptr<T[]>(owner_ptr<T[]>) called.
owner_ptr<T[]>::~owner_ptr() called.
~A() called.
~A() called.
~A() called.
owner_ptr<T[]>::~owner_ptr() called.
owner_ptr<T[]>::~owner_ptr() called.

Test 13:
A() called.
A() called.
A() called.
owner_ptr<T[]>(T*) called.
owner_ptr<T[]>(auto_ptr_ref<T>) called.
owner_ptr<T[]>::~owner_ptr() called.
~A() called.
~A() called.
~A() called.
owner_ptr<T[]>::~owner_ptr() called.

Test 14:
A() called.
A() called.
A() called.
owner_ptr<T[]>(T*) called.
owner_ptr<T[]>(auto_ptr_ref<T>) called.
testFunction<T[]>() called.
owner_ptr<T[]>(owner_ptr<T[]>) called.
owner_ptr<T[]>::~owner_ptr() called.
~A() called.
~A() called.
~A() called.
owner_ptr<T[]>::~owner_ptr() called.
owner_ptr<T[]>::~owner_ptr() called.

Test 15:
A() called.
A() called.
A() called.
A() called.
A() called.
owner_ptr<T[]>(T*) called.
owner_ptr<T[]>::reset(T*) called.
owner_ptr<T[]>::~owner_ptr() called.
~A() called.
~A() called.
~A() called.
owner_ptr<T[]>::~owner_ptr() called.
~A() called.
~A() called.

Test 16:
A() called.
B() called.
owner_ptr<T>(T*) called.
~B() called.
~A() called.

It seems to be working properly. I think, the only problem is that I cannot create a heterogeneous collection like std::vector< owner_ptr<A> > to store derived class instances. But it's not a real problem, because it can be easily solved by creating a similar class to std::shared_ptr which has a real copy constructor.

If you have any idea how can I make this templated class better, before I use it in my own project, please share it. I'm using C++03.

If you think, it is a totally bad idea to inherit from std::auto_ptr, please explain why.

\$\endgroup\$
  • 1
    \$\begingroup\$ Do you have a good reason not to move to a newer language version? Otherwise, this seems like a pointless exercise. I think that even the proprietary compilers support C++14 these days. \$\endgroup\$ – Toby Speight Aug 14 '17 at 13:18
  • \$\begingroup\$ @TobySpeight - I agree with you. But I'm doing this for learning purposes and want to play a bit more with C++03 before I learn C++11 at university. This is my attitude: If you know what C++03 doesn't have you will better understand what C++11 features are good for. \$\endgroup\$ – Gergely Tomcsányi Aug 14 '17 at 13:35
  • 1
    \$\begingroup\$ @GergelyTomcsányi Not exactly, things have been deprecated since C++03, youd learn to appreciate the features offered in c++11+ in learning an entirely different language better than you would with learning c++03, you will pick up bad habits and advice that no longer applies learning it. In fact auto_ptr itself has been deprecated for 6 years, there is literally no point in doing this exercise following that point alone! \$\endgroup\$ – opa Aug 14 '17 at 16:31
  • \$\begingroup\$ @snb - I've read that C++11 is an entirely different language but didn't know that using C++03 can lead me to pick up bad habits. Thanks for mentioning it, I'll keep it in mind! (But I already knew that auto_ptr is deprecated, I won't use it in production code.) \$\endgroup\$ – Gergely Tomcsányi Aug 14 '17 at 16:37
  • \$\begingroup\$ @GergelyTomcsányi I was not suggesting that C++11 is so much different than c++03 that it is an entirely different language, I was suggesting that by learning entirely different languages you would have a much better appreciation of what C++11 offers than learning c++03. This is because C++11 will share features with those languages that you liked, or allow you to do things that those languages would allow you to do, thus you would realize the importance of those features (for example, C++17 now has multiple returns, implemented via tuples. This would be appreciated by Python users) \$\endgroup\$ – opa Aug 14 '17 at 16:39
3
\$\begingroup\$

Do not inherit c++ standard classes (unless these are designed for it1)

If you think, it is a totally bad idea to inherit from std::auto_ptr, please explain why.

They (standard classes) usually aren't designed for inheritance in first place.

Here are some references why this is considered a bad idea:

The main problem is you cannot really change/extend the behavior of the originally exposed interface.


The better way to go is to encapsulate std::auto_ptr as a member, and override the behavior where it doesn't meet your needs (that's lacking of copy/move capabilities it seems).
I know that it's a bit tedious to just provide delegate functions where you don't want to change the original behavior, and tempting to achieve that via inheritance, but in the long term that's usually not what you want.

Beyond that you are able now to introduce copy/move semantics:

 template<typename T>
 class owner_ptr {
     std::auto_ptr<T> pointee;
 public:
     // Will behave like moving
     owner_ptr(const owner_ptr<T>& other) {
         // Brute force:
         T* p = const_cast<owner_ptr<T>&>(other).pointee.release();            
         pointee.reset(p);
     }
 };

Alexei Andrescou's Loki library provides you with a good set of policies you can choose for implementing the various variants of smart pointers. I found that an extremely useful resource for either understanding, and also realize production ready code with help of the library.


1) There are abstract classes in the C++ standard library like std::basic_ostream<T> or std::basic_streambuf. But these are rare cases.

\$\endgroup\$
  • \$\begingroup\$ "// Brute force" sounds fishy. How can you guarantee that the const_cast won't fail? \$\endgroup\$ – πάντα ῥεῖ Aug 14 '17 at 14:13
  • \$\begingroup\$ @πάνταῥεῖ As long you know what you're doing there. I remember doing similar things for maintaining a queue of std::auto_ptr<message> instances when retrieving the auto_ptr from the queue. \$\endgroup\$ – user0042 Aug 14 '17 at 14:29
  • 1
    \$\begingroup\$ @user0042 - Thanks! Change reset to release and your idea will work. (Because reset destroys the owned object, while release doesn't.) Moreover, it also works with std::vector. I find it interesting that removing const doesn't fail. But why? Because you change only the member variable of its member variable (I mean the pointer owned by std::auto_ptr) and you're not changing other directly? \$\endgroup\$ – Gergely Tomcsányi Aug 14 '17 at 14:50
  • \$\begingroup\$ @GergelyTomcsányi Ooops, I've been overlooking that. Yes. \$\endgroup\$ – user0042 Aug 14 '17 at 14:51
  • \$\begingroup\$ @user0042 - So it means that casting a const reference to non-const and calling a non-const function on the referenced object which doesn't modify any of the object's members but changes only member variables of the members of the object is completely allowed and never causes UB? \$\endgroup\$ – Gergely Tomcsányi Aug 14 '17 at 15:05
2
\$\begingroup\$

Unexpected Consequences

The main issue I see with this is:

// Here is another unit test.
// The destructor should be called 15 times But it will only be
// called once on the first element.
void func(std::auto_ptr<int> a)
{
}

int main()
{
    owner_ptr<int[]>  data(new int[15]);
    func(data);
}

Because of the inheritance. This is going to copy the auto pointer to the parameter a and strip all vestiges of owner_ptr. Since it is a copy when a goes out of scope you will again leak.

Undefined Behavior

Here you have undefined behavior:

    if (std::auto_ptr<T>::get() != ptr)
    {
        this->~owner_ptr();              // If you call the destructor
                                         // The object no longer exists.
                                         // So any accesses to members is
                                         // UB.
        std::auto_ptr<T>::reset(ptr);
    }

If you call the destructor on an object you want to keep, you must call the constructor to make the object live again. You can do this with placement new.

         this->~owner_ptr();
         new (this) owner_ptr<T[]>(ptr);

BUT. This looks so ugly. Why not just add a method for deleting the data that is called by this method and the destructor.

Does not Provide Strong Exception Guarantee

Another problem here is if the type T is not trivial.

        this->~owner_ptr();
        std::auto_ptr<T>::reset(ptr);

What happens if an exception is throw (it can happen) during the destructor. They you are left with an object this that has a pointer to invalid data. Note: if a destructor throws during delete then reset() is never called.

To do this in an exception safe manor you must update the state of the current object to be consistent before doing any dangerous operations.

        T* tmp = release();    // get a copy locally (this is nothrow())
        auto_ptr::reset(ptr);  // put the new data into the object
                               // this is also safe as we just released
                               // the held pointer so no destructor will
                               // happen.

        // Your object is now safe (i.e. it is in a consistent good state).
        // It is safe to call the destructor on this object.

        delete [] tmp;
\$\endgroup\$
  • \$\begingroup\$ +1 - I learned something new! I always believed that this->~Object() will call only the function but doesn't destroy the object. Thanks for the exception-safe part too. \$\endgroup\$ – Gergely Tomcsányi Aug 14 '17 at 16:29
  • \$\begingroup\$ @GergelyTomcsányi: It does not "destroy" the object. It ends the "LifeTime" of the object. In the standard n4659 : 4.5-1An object occupies a region of storage in its period of construction, throughout its lifetime and in its period of destruction 6.8-5A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object 6.8-6 after the lifetime of an object has ended , any pointer that represents the address of the storage location where the object will be orwas located may be used but only in limited ways. \$\endgroup\$ – Martin York Aug 14 '17 at 18:07
  • \$\begingroup\$ Copy of current draft standard: open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf \$\endgroup\$ – Martin York Aug 14 '17 at 18:10
-2
\$\begingroup\$

If you need automatic memory management for arrays, use std::vector.

Edit: I disagree with the vote-down. This is a code review. If someone in my department offered up the poster's approach combining arrays with smart pointers, I'd tell him/her to use std::unique_ptr<std::vector<T> >.

\$\endgroup\$
  • 1
    \$\begingroup\$ What do arrays have to do with smart pointer behavior? \$\endgroup\$ – user0042 Aug 14 '17 at 14:54
  • \$\begingroup\$ Smart pointers manage memory instances of objects. For managing memory for arrays of objects, use std::vector. That's why the smart pointers don't have constructors for creating an array of objects. And to manage the memory for the vector, you can use std::auto_ptr<std::vector<T> >. \$\endgroup\$ – RichN Aug 14 '17 at 15:00
  • \$\begingroup\$ Maybe std::auto_ptr< std::vector<T> > would solve many problems regarding arrays. But it also requires T to be a copy-constructible and also assignable type. Moreover, if I'm storing a pack of objects in a std::auto_ptr< std::vector<T> > object BUT don't want to use the features of std::vector then I paid for something which I won't use. Or am I wrong? \$\endgroup\$ – Gergely Tomcsányi Aug 14 '17 at 15:37
  • \$\begingroup\$ @GergelyTomcsányi, I'd wrap vector then, but it will make no sense since just switching to C++11 will solve this. I believe it didn't introduce breaking changes so I don't really understand the need for C++03. It is 14 years old, it is time for it to die already. \$\endgroup\$ – Incomputable Aug 14 '17 at 15:42
  • \$\begingroup\$ @GergelyTomcsányi since a std::vector is a template, the compiler doesn't generate code for methods that aren't used. (C.13.9.1) \$\endgroup\$ – RichN Aug 14 '17 at 21:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.