2

What would be a simple C++ program where a std::vector<std::auto_ptr<T>> compiles but fails to execute correctly, whereas the same program with std::vector<std::unique_ptr<T>> compiles and works correctly, for some data type T?

I know that std::auto_ptr has been deprecated or removed; I just want an example involving containers to motivate why it was deprecated or removed.

I'm using g++-10 -std=c++20 on MacOS Big Sur version 11.2.1.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Mark Meretzky
  • 89
  • 1
  • 6
  • 1
    `std::auto_ptr` is deprecated and one of the main reason - it does not work with containers – Slava Feb 18 '21 at 20:27
  • 1
    Related: https://stackoverflow.com/questions/3697686/why-is-auto-ptr-being-deprecated That question has several answers with examples. – Jerry Jeremiah Feb 18 '21 at 20:28

2 Answers2

6

std::auto_ptr simply cannot be used in standard containers at all. It does not maintain proper semantics under that situation. Which is one of the reasons why move semantics and std::unique_ptr were invented in C++11 in the first place. std::auto_ptr was deprecated in C++11, and removed completely in C++17. So don't use it at all in modern coding.

The official reason why std::auto_ptr was deprecated is detailed here:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1856.html#20.4.5%20-%20Class%20template%20auto_ptr

The example given uses std::sort() on a std::vector<std::auto_ptr<int>>:

With such a design, one could put auto_ptr into a container:

vector<auto_ptr<int> > vec;

However field experience with this design revealed subtle problems. Namely:

sort(vec.begin(), vec.end(), indirect_less());

Depending upon the implementation of sort, the above line of reasonable looking code may or may not execute as expected, and may even crash! The problem is that some implementations of sort will pick an element out of the sequence, and store a local copy of it.

...
value_type pivot_element = *mid_point;
...

The algorithm assumed that after this construction that pivot_element and *mid_point were equivalent. However when value_type turned out to be an auto_ptr, this assumption failed, and subsequently so did the algorithm.

The fix to this problem was to make auto_ptr inhospitable to containers by disallowing "copying" from a const auto_ptr. With such an auto_ptr, one gets a compile time error if you try to put it in a container.

The conclusion at the end comes down to this:

Calling any generic code, whether std or not, that will operate on auto_ptr is risky because the generic code may assume that something that looks like a copy operation, actually is a copy operation.

Conclusion:

One should not move from lvalues using copy syntax. Other syntax for moving should be used instead. Otherwise generic code is likely to initiate a move when a copy was intended.

auto_ptr moves from lvalues using copy syntax and is thus fundamentally unsafe.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 2
    The question is specifically asking for an example in which using of `std::auto_ptr` compiles but fails to execute correctly. It remains to be seen whether that's an appropriate question for SO or not. :-) – erip Feb 18 '21 at 20:30
  • I mean, the first sentence straight-up answers the question. Is a code example really necessary? – sweenish Feb 18 '21 at 20:56
  • Thank you, Remy Lebeau, your example of copying the pivot element in a Quicksort shows very clearly why a container of std::auto_ptr could fail to execute correctly. But then how could a container of std::unique_ptr possibly work correctly with this Quicksort? My type_traits gave identical answers for auto_ptr and unique_ptr (I tried is_copy_constructible, is_move_constructible, is_assignable, is_move_assignable), so how could an algorithm such as a Quicksort tell the difference between auto_ptr and unique_ptr? Thanks again. – Mark Meretzky Feb 20 '21 at 02:06
  • @MarkMeretzky it doesn't have to. When unique_ptr was introduced, the requirements of sort() were also updated to require the algorithm to support move semantics on the values of dereferenced iterators. – Remy Lebeau Feb 20 '21 at 03:43
0

The problem with auto_ptr is that it can be copied and making a copy modifies the original:

a = b;   // transfers ownership from b to a

This is similar to what moving does now, just that at the time of auto_ptr there was no move semantics in the language. Now that C++ has move semantics transfer of ownership can be expressed more clearly:

a = std::move(b);

Nobody would/should expect that b is not modified in that line.

However, with a = b it is commonly assumed, that b is not modified. auto_ptr breaks that assumption. Consider for example:

template <typename P>
void foo() {
    std::vector<P> x;
    x.resize(42);
    int i=0;
    for (auto& e : x) e.reset(new int(i++));
    
    for (auto e : x) {
        std::cout << *e << "\n";        
    }
    for (auto e : x) {
        std::cout << *e << "\n";        
    }    
}

With P=std::unique_ptr<int> this will cause a compiler error:

<source>:17:15: error: call to deleted constructor of 'std::unique_ptr<int, std::default_delete<int> >'
    for (auto e : x) {
              ^ ~

While it compiles with P=std::auto_ptr<int> but is undefined behavior (eg segfault here: https://godbolt.org/z/93hdse), because it dereferences null pointers.

Similar issue with any algorithm assumes it is "ok" to copy elements. For example a comparator for auto_ptr that takes parameters by value does compile but causes havoc:

auto compare = [](auto a,auto b) { return *a < *b; }
std::sort(x.begin(), x.end(),compare);   // BOOM !

Not always is it so obvious that a copy is being made, algorithms may copy elements internally when elements are copyable.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185