How could one implement std::auto_ptr's copy constructor?
Asked Answered
Y

3

6

Back on my crazy AutoArray thingy... (quoting important bits from there:

class AutoArray
{
    void * buffer;
public:
    //Creates a new empty AutoArray
    AutoArray();
    //std::auto_ptr copy semantics
    AutoArray(AutoArray&); //Note it can't be const because the "other" reference
                           //is null'd on copy...
    AutoArray& operator=(AutoArray);
    ~AutoArray();
    //Nothrow swap
    // Note: At the moment this method is not thread safe.
    void Swap(AutoArray&);
};

)

Anyway, trying to implement the copy constructor. There's a piece of client code (not yet committed into bitbucket because it won't build) that looks like this:

AutoArray NtQuerySystemInformation(...) { ... };

AutoArray systemInfoBuffer = NtQuerySystemInformation(...);

This fails because the copy constructor takes a non-const reference as an argument .... but I don't see how you could modify the copy constructor to take a const reference, given that the source AutoArray used in the assignment is modified (and therefore wouldn't be const). You can't modify things to use pass by value of course, because it's the copy constructor and that'd be an infinite loop!

If I was using auto_ptr, this would be valid:

std::auto_ptr NtQuerySystemInformation(...) { ... };

std::auto_ptr systemInfoBuffer = NtQuerySystemInformation(...);

How then, can a class with auto_ptr's copy semantics be possible?

Yousuf answered 22/12, 2010 at 22:0 Comment(4)
Just FYI, your code causes multiple definition errors in VS2008 unless I add inline in front of the std::swap specialization.Anticipate
I notice you keep mentioning thread safety. You can't just pick one method to make thread-safe, though. If you really want this to be thread safe, all access to buffer needs to be atomic. It needs to be a boost::/std::atomic<void*> instead. Imagine someone calls swap while another thread just accesses it with some other method, you'd have a problem. (Again, swap will likely need a mutex, so you'd just mutex every method.) But just wanted to make sure you weren't going to try to protect one function and not any others.Loferski
@GMan: When I saw that it was impossible to make a thread-safe swap I threw out the pretense of thread safety on this. If someone wants thread safety on this they're probably better off controlling it using synchronization primitives at a higher level than my crappy WindowsApi::AutoArray class :)Yousuf
@Praetorian: Yep -- didn't notice that problem until I tested with more than one object file.Yousuf
H
14

auto_ptr uses a dirty trick.

I'll use a dumbed-down class named auto_int to demonstrate just the copy construction functionality without bringing in any complexities introduced by templates or inheritance. I think the code is mostly correct, but it's untested. Our basic auto_int look something like this:

class auto_int
{
public:

    auto_int(int* p = 0) : p_(p) { }

    ~auto_int() { delete p_; }

    // copy constructor taking a non-const reference:
    auto_int(auto_int& other) 
        : p_(other.release()) { }

    int* release() 
    {
        int* temp = p_;
        p_ = 0;
        return temp;
    }

private:

    int* p_;
};

With this basic auto_int, we can't copy a temporary object. Our goal is to be able to write something like:

auto_int p(auto_int(new int()));

What we can do is use a helper class. For auto_ptr, this is called auto_ptr_ref. We'll call ours auto_int_ref:

class auto_int;

class auto_int_ref 
{
public:
    auto_int_ref(auto_int* p) : p_(p) { }

    auto_int& ref() { return *p_; }

private:
    auto_int* p_;
};

Basically, an instance of this class just stores a pointer to an auto_int and allows us to use it as a "reference" to an auto_int.

Then in our auto_int class we need two additional functions. We need another constructor that takes an auto_int_ref and we need a conversion operator that allows an auto_int to be implicitly converted to an auto_int_ref:

auto_int(auto_int_ref other)
    : p_(other.ref().release()) { }

operator auto_int_ref() { return this; }

This will allow us to "copy" a temporary while still having the copy constructor take a non-const reference. If we look again at our sample code:

auto_int p(auto_int(new int()));

What happens is we construct a new temporary auto_int and pass new int() to the constructor that takes an int*. This temporary is then converted to an auto_int_ref that points to it, using the operator auto_int_ref(), and the auto_int constructor that takes an auto_int_ref is used to initialize p.

Hellcat answered 22/12, 2010 at 22:14 Comment(10)
+1. Didn't notice before that auto_ptr_ref is involved in return by value even when the template parameter does not change. I always thought it was just for converting auto_ptr<Derived> to auto_ptr<Base>.Murrey
Note that this trick (called the Colvin-Gibbons trick) has been superseded by proper move semantics in C++0x. The auto_ptr class is deprecated in favor of unique_ptr.Hauteur
@In silico: Yes -- if I could use C++0x I'd be done. But unfortunately I need to support Win2k, which means I need to build using VS2008, which means I don't get C++0x features.Yousuf
@Billy ONeal: Right, it's a note to everyone else who are using a C++0x compilers. :-)Hauteur
@In silico: Thanks for the Colvin-Gibbons name; I wasn't familiar with that. It's a fun trick.Hellcat
@James McNellis: It's a fun trick indeed. Especially since it's an apparent "violation" of the "one user-defined implicit conversion" rule (it requires 2 or 3 conversions for it to work). I believe the wording in the current C++ standard has been already changed a while ago to allow auto_ptr to "work".Hauteur
Boost.Ref is essentially this trick extracted and served as a standalone for reuse.Gilgai
@James McNellis: Implemented ( hpp cpp ). Thanks!Yousuf
In my view, auto_int(auto_int&) is a move-constructor because (1) it takes a non-const reference and (2) it clears the state of the RHS object. But then why isn't the implicitly generated copy-constructor auto_int(const auto_int&) called in the case of auto_int p(auto_int(new int())); ?Insanitary
@Sumant: Yes, this is a primitive, pre-C++11 way to get move-like semantics. There is no implicitly-declared copy constructor in this scenario because there is a user-declared copy constructor that suppresses it: auto_int(auto_int&) is a copy constructor.Hellcat
W
3

auto_ptr's copy ctor works by taking ownership away from the passed-in object. This is also a big part of the reason why auto_ptr can't be used in a vector or other STL collections.

This is not how most copy constructors work. Usually your copy constructor will just clone the passed-in object, so you can pass a const reference to it. But auto_ptr doesnt do this. It actually modified the original object. In this sense, its only a copy constructor by name, not by semantics.

EDIT2:

I'm trying to boil this down a bit. So effectively you're trying to do something with your class that will allow syntax similar to this:

#include <string>
#include <memory>
using namespace std;

auto_ptr<string> gimme()
{
    return auto_ptr<string>(new string("Hello"));
}

int main()
{
    auto_ptr<string> s = gimme();
}

...and you're wondering how to get the s = gimme() part to work. Right?

The secret here is in a proxy class, auto_ptr_ref described by the Standard in 20.4.5 whose purpose is "to allow auto_ptr objects to be passed to and returned from functions.":

namespace std {
  template <class Y> struct auto_ptr_ref {};
  template<class X> class auto_ptr {
  public:
    typedef X element_type;

    // 20.4.5.1 construct/copy/destroy:
    explicit auto_ptr(X* p =0) throw();
    auto_ptr(auto_ptr&) throw();
    template<class Y> auto_ptr(auto_ptr<Y>&) throw();
    auto_ptr& operator=(auto_ptr&) throw();
    template<class Y> auto_ptr& operator=(auto_ptr<Y>&) throw();
    auto_ptr& operator=(auto_ptr_ref<X> r) throw();
    ˜auto_ptr() throw();
    // 20.4.5.2 members:
    X& operator*() const throw();
    X* operator->() const throw();
    X* get() const throw();
    X* release() throw();
    void reset(X* p =0) throw();
    // 20.4.5.3 conversions:
    auto_ptr(auto_ptr_ref<X>) throw();
    template<class Y> operator auto_ptr_ref<Y>() throw();
    template<class Y> operator auto_ptr<Y>() throw();
  };
}
Whoa answered 22/12, 2010 at 22:5 Comment(4)
Yes, I know how auto_ptr works. How would one implement the copy constructor though? The function call in the client code like I have above works if I change the function to return an auto_ptr, but it doesn't work if I have it return AutoArray. auto_ptr can't take a const reference to it's copy constructor either, because it modifies the argument from which it's being copied.Yousuf
(In response to edit): I know it's only a copy constructor in name, but the code I have above (returning an auto_ptr from a function) certainly works/is valid.Yousuf
@Billy: Maybe I'm getting stuck on "How then, can a class with auto_ptr's copy semantics be possible?" but I'm not understanding how I didn't answer your question? We must be talking about different things?Whoa
I can return an auto_ptr from a function by value. That requires that the auto_ptr need to have some form of copy constructor that lets you assign the return value of the function into some auto_ptr variable. But with a plain non-const copy constructor, that's not possible, because the return value from the function is a temporary, and you're not allowed to bind a reference to a temporary. So there's got to be some non-obvious form of proxy/template magic going on inside auto_ptr that makes it work...Yousuf
M
0

There is no implicit conversion from T* to std::auto_ptr<T>. I'm guessing you do have an implicit conversion constructor from NTSTATUS handle to AutoArray. But if that conversion creates a temporary, that temporary can't be copied.

If you use direct initialization rather than copy initialization, your "problem" may go away.

AutoArray systemInfoBuffer( ntDll.NtQuerySystemInformation(
  Dll::NtDll::SystemProcessInformation) );
Murrey answered 22/12, 2010 at 22:13 Comment(3)
NtQuerySystemInformation in this case is not the one defined inside ntdll.dll, it's a wrapper function I wrote to abstract away buffer management of the actual call in Ntdll.dll. Changing assignment out with a more direct copy constructor call doesn't fix the problem.Yousuf
Ah, oops. Might have noticed the function documentation wasn't a class member. So does the function return an AutoArray by value? Or does it return, say, a different specialization of the same primary class template?Murrey
Yes, it returns an AutoArray by value.Yousuf

© 2022 - 2024 — McMap. All rights reserved.