21

This answer cites N4082, which shows that the upcoming changes to std::shared_ptr will allow both T[] and T[N] variants:

Unlike the unique_ptr partial specialization for arrays, both shared_ptr<T[]> and shared_ptr<T[N]> will be valid and both will result in delete[] being called on the managed array of objects.

 template<class Y> explicit shared_ptr(Y* p);

Requires: Y shall be a complete type. The expression delete[] p, when T is an array type, or delete p, when T is not an array type, shall be well-formed, shall have well defined behavior, and shall not throw exceptions. When T is U[N], Y(*)[N] shall be convertible to T*; when T is U[], Y(*)[] shall be convertible to T*; otherwise, Y* shall be convertible to T*.

Unless I'm mistaken, a Y(*)[N] could only be formed by taking the address of an array, which clearly can't be owned or deleted by a shared_ptr. I also don't see any indication that N is used in any way to enforce the size of the managed object.

What is the motivation behind allowing the T[N] syntax? Does it yield any actual benefit, and if so, how is it used?

Community
  • 1
  • 1
monkey0506
  • 2,489
  • 1
  • 21
  • 27
  • 4
    Nothing protects you from misuse, but once you have a `shared_ptr`, you can iterate over its elements without additional information, so there's that. Note that you still construct it with a pointer to the first element, never with the pointer to the array itself, since you're suppose to get the pointer from `new T[N]`. – Kerrek SB Nov 06 '16 at 09:37
  • @KerrekSB "... *since you're suppose to get the pointer from new* `T[N]`" Not always. See [my answer](http://stackoverflow.com/a/40447905/6394138). – Leon Nov 06 '16 at 09:48
  • As I reviewed N4082 further, I do see that `operator[]` states `i < N` as a precondition, but it is also `noexcept`, so I guess that still leaves `ptr[N]` as undefined behavior. – monkey0506 Nov 06 '16 at 10:20
  • @KerrekSB: actually, using an array type with the aliasing constructor of `std::shared_ptr` is a use where you actually need `std::shared_ptr`. – Dietmar Kühl Nov 06 '16 at 11:12
  • 2
    It's worth reading [N3920](http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2014/n3920.html), which contains rationale and explanation. N4082 is just gathering individual proposals into a coherent plan for Library Fundamentals. – Toby Speight Nov 08 '16 at 11:18

2 Answers2

6

You can get a pointer to a nested object sharing ownership with a std::shared_ptr to the containing object. If this nested object happens to be an array and you want to access it as an array type, you actually need to use T[N] with suitable T and N:

#include <functional>
#include <iostream>
#include <iterator>
#include <memory>
#include <queue>
#include <utility>
#include <vector>

using queue = std::queue<std::function<void()>>;

template <typename T>
struct is_range {
    template <typename R> static std::false_type test(R*, ...);
    template <typename R> static std::true_type test(R* r, decltype(std::begin(*r))*);
    static constexpr bool value = decltype(test(std::declval<T*>(), nullptr))();
};

template <typename T>
std::enable_if_t<!is_range<T>::value> process(T const& value) {
    std::cout << "value=" << value << "\n";
}

template <typename T>
std::enable_if_t<is_range<T>::value> process(T const &range) {
    std::cout << "range=[";
    auto it(std::begin(range)), e(std::end(range));
    if (it != e) {
        std::cout << *it;
        while  (++it != e) {
            std::cout << ", " << *it;
        }
    }
    std::cout << "]\n";
}

template <typename P, typename T>
std::function<void()> make_fun(P const& p, T& value) {
    return [ptr = std::shared_ptr<T>(p, &value)]{ process(*ptr); };
                            // here ----^
}

template <typename T, typename... M>
void enqueue(queue& q, std::shared_ptr<T> const& ptr, M... members) {
    (void)std::initializer_list<bool>{
        (q.push(make_fun(ptr, (*ptr).*members)), true)...
        };
}

struct foo {
    template <typename... T>
    foo(int v, T... a): value(v), array{ a... } {}
    int value;
    int array[3];
    std::vector<int> vector;
};

int main() {
    queue q;
    auto ptr = std::make_shared<foo>(1, 2, 3, 4);
    enqueue(q, ptr, &foo::value, &foo::array, &foo::vector);
    while (!q.empty()) {
        q.front()();
        q.pop();
    }
}

In the above code q is just a simple std::queue<std::function<void()>> but I hope you can imagine that it could be a thread-pool off-loading the processing to another thread. The actually scheduled processing is also trivial but, again, I hope you can imagine that it is actually some substantial amount of work.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • 1
    Perhaps I've missed some part of your example here, but I don't see why `shared_ptr` is necessary for this example. The example works just as well with `shared_ptr` and `shared_ptr` (assuming `std::experimental::shared_ptr` for the latter), with the only other modification being to let `f->array` decay instead of taking its address with `&`. And seeing as the `int(*)[42]` has to decay into `element_type*` (that is, `int*`) to match the function call anyway, there's still no obvious benefit... – monkey0506 Nov 06 '16 at 13:13
  • @monkey_05_06: for concrete code it is easy to drop the address-of operator. In generic code dealing with arrays would be a special case which can (and is) easily avoided. – Dietmar Kühl Nov 07 '16 at 21:01
  • Could you show an example where generic code makes it any less simple to drop the address-of operator? Array decay is implicit, so I can't think of any example where making the distinction between `T(*)[N]` and `T*` would be useful. In this case, `N` is explicitly **not** stored anywhere, even in the `T[N]` specialization, so you can't even make the argument that `N` is being forwarded to some `size` parameter (because there isn't any). – monkey0506 Nov 08 '16 at 06:13
  • Effectively, I'm asking you to provide an edit to your answer which actually presents a use-case (of which you admitted that you don't, or didn't, have one). The reason I'm asking for a use-case is because your answer doesn't present any behavior which requires or otherwise benefits whatsoever from the `T[N]` specialization, and if you can't present behavior which does then you haven't answered the question. – monkey0506 Nov 08 '16 at 06:16
  • @monkey_05_06: all the ingredients to an actually use were in the answer. Thus, I disagree that I didn't answer the question. I have update the answer to make it more obvious where the facility is used. Despite your argument that the size isn't actually possibly used it turns out that the size does come in handy. – Dietmar Kühl Nov 08 '16 at 10:46
  • @Yakk: what document are you looking at? I think, support for array types is going into C++17 and it seems the specification in [N4606](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4606.pdf) is this (20.11.2.2.5 [util.smartptr.shared.ops] paragraph 4: "*Remarks*: When `T` is (possibly cv-qualified) `void`, it is unspecified whether this member function is declared. If it is declared, it is unspecified what its return type is, except that the declaration (although not necessarily the definition) of the function shall be well formed." (note that the array treatment was removed) – Dietmar Kühl Nov 08 '16 at 22:03
  • @DietmarKühl I was reading [N4082](http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2014/n4082.pdf). Mea culpa! – Yakk - Adam Nevraumont Nov 08 '16 at 22:08
  • Your `process` function overload is taking a parameter of type `T(&)[N]`, but there's no reason it couldn't just as easily accept two parameters, `T *array, std::size_t N` -- **without** even changing the body of the function. You add *some* type security, but to make your "*generic*" functions *actually* generic, you'd still have to provide an overload/specialization for heap-allocated arrays in **addition** to this stack-allocated array overload. Arguably, your implementation is **worse** as it adds an unnecessary overload that could be avoided simply by changing the parameter list. – monkey0506 Nov 09 '16 at 08:09
  • @monkey_05_06: the generic function happens to be `make_fun()`. Sure, the code can be done differently, e.g., by having two versions of `make_fun()` so processing of array members adds the extra size parameter. That's besides the point, though. I made `process()` generic, too, although doing so just adds clutter unrelated to the actual discussion. – Dietmar Kühl Nov 09 '16 at 14:13
  • So, I was wrong about being wrong, maybe? Your linked standard lacks the changes for array shared ptr support proposed? – Yakk - Adam Nevraumont Nov 12 '16 at 00:10
2

Unless I'm mistaken, a Y(*)[N] could only be formed by taking the address of an array, which clearly can't be owned or deleted by a shared_ptr.

Don't forget that shared_ptr is a generic resource management utility and can be constructed with a custom deallocator:

template<class Y, class D> shared_ptr(Y* p, D d);

Such a user-provided deallocator can perform an action other than delete/delete[]. For example if the the array under question is an array of file descriptors, the "deallocator" can just close all of them.

In such cases the shared_ptr doesn't own the object in the widely used sense and therefore can be bound to an existing array by taking its address.

Leon
  • 31,443
  • 4
  • 72
  • 97
  • Is there any text in the standard which justifies that "`shared_ptr` is a generic resource management utility"? As I have always understood it, `shared_ptr` implies a contract of ownership. The need to allow custom deleters means that contract can't be strictly enforced, but I have always viewed it as an error to have non-owning `shared_ptr`s. – monkey0506 Nov 06 '16 at 09:55
  • 1
    @monkey_05_06 The support for custom deallocators makes it a generic resource management utility. You only know about a shared pointer that the last pointer referring to an object will perform the clean-up action (which by default is deleting the object, but in principle can be just anything) before that reference is removed. – Leon Nov 06 '16 at 10:03
  • Maybe you could say it's an argument of semantics, but the existence of methods like `std::get_deleter` suggest that a `delete` operation is a contractual part of `shared_ptr` (further supported by the fact that `operator delete` is the default deleter). I'm not arguing whether you *can* produce non-owning `shared_ptr`s, but I maintain that doing so is a bad thing to do in practice. I feel that it obfuscates the code to use `shared_ptr` as a reference wrapper. – monkey0506 Nov 06 '16 at 10:14
  • Actually, I've just remembered [observer_ptr](http://en.cppreference.com/w/cpp/experimental/observer_ptr), which takes the implied ownership contract of `shared_ptr` a step further by providing an entire separate class for non-owning observers. – monkey0506 Nov 06 '16 at 10:16