7

I explored this topic in Coliru with the following input command:

g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

The test can be found here, but I have posted the code below. I used int in my example, as it's a basic type.


#include <iostream>
#include <memory>

struct Foo{
    Foo() :
    a_{0}, b_{1}, c_{-1}, combination_{0.5} {}

    int
        a_,
        b_,
        c_;
    double
        combination_;
};

int main()
{
    //int
    //    *unManagedArray = new int[16];
    std::unique_ptr<int[]>
        uniqueArrayOrigin = std::make_unique<int[]>(16);
    std::shared_ptr<int>
            // works but needs call to new
    //  sharedSingleTest{unManagedArray, std::default_delete<int[]>{}}; 
            // works, does not require call to new
        sharedSingleUnique = std::make_unique<int[]>(16);       
            // compilation error (conversion to non-scalar type)
    //  sharedSingleDerived = uniqueArrayOrigin;                

    //  std::shared_ptr<int[]>
                // compilation errors
    //      sharedArrayTest{unManagedArray, std::default_delete<int[]>{}};
                // compilation error (conversion to non-scalar type)
    //      sharedArrayUnique = std::make_unique<int[]>(16);
                // compilation error (conversion to non-scalar type)
    //      sharedArrayDerived = uniqueArrayOrigin;

    std::shared_ptr<Foo>
            // works: specified overload of operator= for shared_ptr
        nonArrayTest = std::make_unique<Foo>(); 

    std::cout << "done!\n";
}

I have looked around on SO for answers, but only turned up references to the implementation of std::shared_ptr not having a specialization, and that this largely was because no one bothered to give a proper proposal to the standards committee on the subject.

I am curious, because I would interpret 4th overload of operator=, std::shared_ptr<T[]>.operator=(std::unique_ptr<T[], Deleter>&&) on cppreference to indicate that such syntax is legal-- T[] and T[] are the same type regardless of the state of specializations for array types for std::shared_ptr, after all.

Furthermore, this syntax only appears to work on the product of std::make_unique<T[]>, and not a unique pointer object, which goes against my understanding of the topic--shouldn't the calls be effectively the same, though one moves an existing object, and the other, well, moves an object that's just been created? I would expect the only difference between them would be the invalid std::unique_ptr<T[]> after the function call in the first case.

As a side note, I assume that since there is a way of constructing a dynamically-allocated array into a shared_ptr that does not require the use of new, I should prefer it to the messier and exception-unsafe call to new T[N]?

tl;dr:

  1. operator= does not work at all between std::shared_ptr<T[]> and std::unique_ptr<T[]> though I would expect it to work. Why?
  2. If anything, I would expect the type conversion from T[] to T to be a source of compilation errors between the unique and shared pointers. Why does this work?
  3. operator= works between std::shared_ptr<T> and std::make_unique<T[]> but not std::unique_ptr<T[]>. Why?
  4. am I correct to assume in cases which require a dynamically allocated, shared array, but where I don't want to use either boost or a vector (reasons below) I should call operator= std::make_unique<T[]>(N)?

Why aren't I using?

  • Boost: is not approved for use yet in my company, and I do not know when or if I will get approval to use it.
  • Arrays: I have to determine the size of this array at runtime.
  • Vectors: I'm working on a real-time signal processing system, and would prefer to avoid the extra pointer dereference. I was also attempting to avoid including extraneous libraries in my header files (this was for communications between reading and writing subsystems) However, I eventually chose to optimize this later, if it matters (premature optimization...) and bite the bullet. The question remains, though.
Community
  • 1
  • 1
jaggedSpire
  • 4,423
  • 2
  • 26
  • 52
  • 4
    Currently, `std::shared_ptr` doesn't support arrays. – T.C. May 12 '15 at 16:52
  • To clarify:It's not merely the case that a specialization for array types doesn't exist, but that `std::shared_ptr` actively doesn't support arrays? I had assumed that the lack of a specialization meant that it acted on array types like they were any other type. – jaggedSpire May 12 '15 at 17:05

1 Answers1

5

§20.8.2.2.1/28:

template <class Y, class D> shared_ptr(unique_ptr<Y, D>&& r); 

Remark: This constructor shall not participate in overload resolution unless unique_ptr<Y, D>::pointer is convertible to T*.

However, unique_ptr<U[]>::pointer is actually U*, while shared_ptr<U[]>'s T* is U(*)[]; And U* cannot be converted to U(*)[], hence the overload is never considered.

Columbo
  • 60,038
  • 8
  • 155
  • 203
  • @Yakk So because the behavior of an unspecialized `shared_ptr` of type `T[]` is not what I might expect, it's best to simply regard them as simply not being supported as T.C. said, rather than simply lacking a few nice additions to functionality? – jaggedSpire May 12 '15 at 17:51
  • 1
    @jaggedSpire Precisely. Also consider that `shared_ptr` never calles `delete[]`. – Columbo May 12 '15 at 17:58
  • Why wouldn't it call the proper deleter when supplied with the deleter from the properly specialized `unique_ptr` object? Also, may I inquire further why there is a functional difference in constructing the `shared_ptr` in question from an existing `unique_ptr` and from the product of `make_unique`? – jaggedSpire May 12 '15 at 18:08
  • @Yakk `U (*)[]` is a pointer to an array, not a pointer to a pointer. A pointer to an array points to the same address as a pointer to the array's first element. –  May 12 '15 at 18:45
  • @Yakk `U[]` is an array type of unknown bound, but an array type nevertheless, so why would @hvd be wrong? – Columbo May 12 '15 at 19:17
  • 1
    @Yakk `U(*)[]` doesn't decay to `U**`, but if you have an expression `u` of type `U(*)[]`, then `*u` has type `U[]` which decays to `U*`, and `**u` (or `(*u)[0]`) can be used to access the elements. A minimal complete program that uses this, without any dynamic allocations, is `extern int zero[]; int main() { int (*pzero)[] = &zero; return **pzero; }` in TU 1, and `int zero[] = { 0 };` in TU 2. –  May 12 '15 at 19:31
  • @hvd @Yakk @Columbo While I am learning from this discussion on the subtleties of types in C++, should I make a different question about the difference between a preexisting `unique_ptr` and `make_unique` to initialize a `shared_ptr`, only one of which doesn't produce a compilation error? It seems like that question might have drowned in the rest of my question. – jaggedSpire May 12 '15 at 19:39
  • @hvd Hmm, did not know that. My attempts to use `int(*)[]` failed miserably, and I didn't try to double-dereference it, nor did I think of `extern`. – Yakk - Adam Nevraumont May 12 '15 at 19:40
  • 2
    @jaggedSpire The only difference there is between assignment from an lvalue of type `unique_ptr`, and assignment from an rvalue of type `unique_ptr`. Types may choose to allow only one, or only the other, and `unique_ptr` is a type that makes use of this. Assignments from lvalues generally require copies to be made, and `unique_ptr` cannot be copied, or the pointer would no longer be unique. –  May 12 '15 at 19:43
  • @hvd Of course! I forgot to use the `move` wrapper for the existing `unique_ptr` in my tests. It (naturally) works as well as the call to `std::make_unique` if I do that. Everything makes much more sense now, thank you. – jaggedSpire May 12 '15 at 19:49