Is it true that a unique_ptr declaration, unlike a auto_ptr declaration, is well-defined when its template type is of an incomplete type?
Asked Answered
T

2

12

I wrote this article and got some comments on it that confused me.

It basically boils down to my having seen T2 used only as a template parameter and mistakenly jumped to the conclusion that I could therefore take the opportunity of forward declaration:

struct T2;

struct T1
{
    std::auto_ptr<T2> obj;
};

This invokes UB if I don't go on to define T2 somewhere in the same TU, because std::auto_ptr<T2> calls delete on its internal T2*, and calling delete on an pointer to an object of an incomplete type whose complete type has a non-trivial destructor is undefined:

[C++11: 5.3.5/5]: If the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.

The GCC toolchain I happened to be using — v4.3.3 (Sourcery G++ Lite 2009q1-203) — was kind enough to let me know with a note:

note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined.

though it seems difficult to get this diagnostic in other GCC versions.

My gripe was that it would be a lot easier to spot a bug like this if deleteing a pointer to an instance of an incomplete type were ill-formed rather than UB, but that seems like an intractible problem for an implementation to solve, so I understand why it's UB.

But then I'm told that, if I were to use std::unique_ptr<T2> instead, this would be safe and compliant.

n3035 allegedly says at 20.9.10.2:

The template parameter T of unique_ptr may be an incomplete type.

All I can find in C++11 proper is:

[C++11: 20.7.1.1.1]:

/1 The class template default_delete serves as the default deleter (destruction policy) for the class template unique_ptr.

/2 The template parameter T of default_delete may be an incomplete type.

But, default_delete's operator() does require a complete type:

[C++11: 20.7.1.1.2/4]: If T is an incomplete type, the program is ill-formed.


I suppose my question is this:

Are the commenters on my article correct in saying that a translation unit consisting of only the following code is well-formed and well-defined? Or are they wrong?

struct T2;

struct T1
{
    std::unique_ptr<T2> obj;
};

If they are correct, how is a compiler expected to implement this, given that there are good reasons for it being UB, at least when an std::auto_ptr is used?

Thermy answered 7/10, 2012 at 0:1 Comment(2)
Because the wording is 'the program is ill-formed' (with no explicit 'and no diagnostic is required') and not e.g. 'this results in undefined behavior', this does mean that a diagnostic is required (which can be implemented as static_assert( sizeof(T), "" )). So yes, std::unique_ptr<T> is safer than std::auto_ptr<T>. (But do note that's due to the use of the default std::default_delete<T> deleter.)Roobbie
I wanted to make it clear that 'the code is well-formed and well-defined'/'there is UB' aren't the only two possible outcomes. (It may not be what you wanted to convey but I'd rather be explicit.)Roobbie
I
9

According to Herb Sutter in GOTW #100, unique_ptr suffers from the same problem as auto_ptr with respect to incomplete types.

...although both unique_ptr and shared_ptr can be instantiated with an incomplete type, unique_ptr’s destructor requires a complete type in order to invoke delete...

His suggestion is to declare the destructor of your containing class (i.e. T1) in the header file, then place it's definition in a translation unit in which T2 is a complete type.

// T1.h
struct T2;

struct T1
{
  ~T1();
  std::unique_ptr< T2 >;
};

// T1.cpp
#include "T2.h"

T1::~T1()
{
}
Impalpable answered 7/10, 2012 at 0:14 Comment(2)
Only std::unique_ptr<T> has this property, not std::unique_ptr altogether, and I wouldn't call it a problem. If you're okay with paying the costs of type-erasure you can use e.g. an std::unique_ptr<T, std::function<void(T*)>> and pass an appropriate deleter at construction time.Roobbie
An alternative is std::unique_ptr<T, void(*)(T*)> (together with e.g. a capture-less lambda) where the cost is instead an indirection.Roobbie
S
8

The following example is an attempt to demonstrate the difference between std::auto_ptr<T> and std::unique_ptr<T>. First consider this program consisting of 2 source files and 1 header:

The header:

// test.h

#ifndef TEST_H
#define TEST_H

#include <memory>

template <class T>
using smart_ptr = std::auto_ptr<T>;

struct T2;

struct T1
{
    smart_ptr<T2> obj;

    T1(T2* p);
};

T2*
source();

#endif  // TEST_H

First source:

// test.cpp

#include "test.h"

int main()
{
    T1 t1(source());
}

Second source:

// test2.cpp

#include "test.h"
#include <iostream>


struct T2
{
    ~T2() {std::cout << "~T2()\n";}
};

T1::T1(T2* p)
    : obj(p)
{
}

T2*
source()
{
    return new T2;
}

This program should compile (it may compile with a warning, but it should compile). But at run time it demonstrates undefined behavior. And it probably won't output:

~T2()

which indicates that T2's destructor has not been run. At least it doesn't on my system.

If I change test.h to:

template <class T>
using smart_ptr = std::unique_ptr<T>;

Then the compiler is required to output a diagnostic (an error).

That is, when you make this mistake with auto_ptr you get a run time error. When you make this mistake with unique_ptr you get a compile time error. And that is the difference between auto_ptr and unique_ptr.

To fix the compile time error you must outline ~T1() after T2 is complete. In test2.cpp add after T2:

T1::~T1() = default;

Now it should compile and output:

~T2()

You will likely want to declare and outline move members as well:

T1::T1(T1&&) = default;
T1& T1::operator=(T1&&) = default;

You could make these same fixes with auto_ptr and it would again be correct. But again, the difference between auto_ptr and unique_ptr is that with the former, you don't find out until run time that you have some debugging to do (modulo optional warnings your compiler may give). With the latter you are guaranteed to find out at compile time.

Schnook answered 7/10, 2012 at 2:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.