1

I'm working on (yet another) C++ serialization library that supports standard types such as containers. In particular I want to support smart pointers.

C++17 introduced support for std::shared_ptr holding raw arrays (it knows to invoke delete [] in such cases). I need to detect that the shared_ptr is holding a raw array so that I can serialize it accordingly:

template <typename T>
void serialize(Writer& writer, const std::shared_ptr<T> ptr)
{
    // Writer has overloaded operator()

    if (ptr)
    {
        if (holdsRawArray(ptr)) // How to implement this???
        {
            auto arrayWriter = writer.array(); // RAII
            auto size = rawArraySize(ptr); // How to get this???
            for (std::size_t i=0; i<size; ++i) 
                arrayWriter(ptr[i]);
        }
        else
            writer(*ptr);
    }
    else
        writer(null);
}

How do I determine that a C++17 smart pointer contains a raw array? This information is erased in the element_type member typedef (via std::remove_extent_t). I also can't find anything in the smart pointer API that would reveal the size of the raw array.

I thought about using the detector idiom on operator[] and operator*, but it appears implementations are not required to undefine them if T is or isn't a raw array.

Is what I'm attempting even possible? I'm hoping I missed something, or that there's some trick I could use.

I know I can force users to use std::shared_ptr<std::array<N,T>> or std::shared_ptr<std::vector<T>> instead, but I just want to check if it's even possible for me to support smart pointers holding raw arrays.

Emile Cormier
  • 28,391
  • 15
  • 94
  • 122
  • There's nothing in the `shared_ptr` interface, for this. – Sam Varshavchik Apr 10 '20 at 17:31
  • "I also can't find anything in the smart pointer API that would reveal the size of the raw array." you would not, as there is no way for smart pointer to get that information. – Slava Apr 10 '20 at 17:39
  • @Slava I think the smart pointer can get the size information upon construction, but it appears it doesn't store it anywhere (I hope I'm mistaken about the latter). – Emile Cormier Apr 10 '20 at 17:43
  • 1
    " I think the smart pointer can get the size information upon construction" ok let's see `std::shared_ptr ptr( new int[10] );` show me the way shared ptr can get value 10 in this case – Slava Apr 10 '20 at 17:45
  • @Slava I see your point. I haven't used dynamic raw arrays in ages, so I forgot about the syntax. – Emile Cormier Apr 10 '20 at 17:50
  • Even if the array's size could be somehow recorded, this is runtime information that would take up space inside the smart pointer, and the added size cost to hold this information would probably be unacceptable to most. I now see how silly my question is, hehe. – Emile Cormier Apr 10 '20 at 18:01
  • Before C++17 you could probably get `shared_ptr` deleter and compare it to deleter on array. – ks1322 Apr 10 '20 at 18:34
  • 1
    You can know the size of `std::shared_ptr` but not for `std::shared_ptr`. – Jarod42 Apr 10 '20 at 19:13
  • @Jarod42 So all I'd need is this overload, then: ` – Emile Cormier Apr 10 '20 at 19:24
  • I tried `std::shared_ptr` and it works in GCC 7.5. With Clang 8.0, I get "(aka 'const std::__1::shared_ptr') does not provide a subscript operator". It seems the version of libc++ I'm using doesn't yet have shared_ptr support for raw arrays as spec'd by the C++17 standard. – Emile Cormier Apr 10 '20 at 21:14
  • @Jarod42 According to this [answer](https://stackoverflow.com/a/16597578/245265), `unique_ptr` is ill-formed, and indeed doesn't work for me in GCC. I'm wondering if `shared_ptr` is also ill-formed and that it's a "fluke" that it works for me in GCC. – Emile Cormier Apr 10 '20 at 22:38
  • I found P0674R1 (Extending make_shared to Support Arrays) and it provides several examples of `shared_ptr`, so it appears to be legal assuming that the committee didn't disallow it when it was adopted. – Emile Cormier Apr 10 '20 at 22:49
  • 1
    [constructor doc](https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr) (3-7) has C++17 note about participating overloads when `T` is either `U[N]` or `U[]`. – Jarod42 Apr 11 '20 at 09:40
  • @Jarod42 I saw that note, but wasn't sure if `U[N]` was still allowed as the `shared_ptr` template parameter. – Emile Cormier Apr 11 '20 at 18:16

2 Answers2

2

You can determine if shared_ptr holds an array type by checking if T is an array type using compile-time type traits. It is even implemented in std.

if constexpr (std::is_array_v<T>)

But there is no way to get the size because it is allocated dynamically and not stored anywhere.

sparik
  • 1,181
  • 9
  • 16
  • 2
    Even if you could get the size, some of the entries might not be initialized, so accessing them could well be UB. This is just a bad idea all around for a lot of reasons. – David Schwartz Apr 10 '20 at 18:36
  • 1
    The funniest thing is that `new []` does store the size because `delete []` needs to know how many destructors to invoke. It's just stored in an undocumented way. See [this](https://stackoverflow.com/a/198116/485343) for example (don't use it!!). – rustyx Apr 10 '20 at 18:50
  • @sparik Indeed, I was poking in libc++ and found `std::is_array` in the `shared_ptr` constructor overloads. I have access to `T` even if `element_type` erases the array type, so `std::is_array` is possible in my case. Alas, the lack of size information is the deal breaker as you and others have pointed out. Thanks! – Emile Cormier Apr 10 '20 at 18:57
  • @DavidSchwartz doesn't `new Foo[10]` invoke the constructor on all elements? Or is your point that a user could "lie" about the elements being initialized when passing the `shared_ptr` to the serializer? – Emile Cormier Apr 10 '20 at 19:28
  • @DavidSchwartz Oh I see, the user could do shared_ptr(new Foo[n]) and not bother to check that n==10, and I would have no way of knowing. – Emile Cormier Apr 10 '20 at 20:05
  • @DavidSchwartz I also realize now that `shared_ptr(new int[10])` may/will not contain initialized values. My library should not be encouraging its use. – Emile Cormier Apr 10 '20 at 21:31
0

As commented by Jarod42, I can have access to the raw array size if std::shared_ptr<T[N]> is used (but not std::shared_ptr<T[]>).

I can therefore add a serialize overload to obtain the size N via template parameters, and at the same time know that the shared_ptr is holding an array.

#include <iostream>
#include <memory>
#include <string>
#include <type_traits>

struct Writer // Toy example
{
    template <typename T>
    void operator()(const T& v) {std::cout << v << ", ";}
};

struct ArrayWriter  // Toy example
{
    ArrayWriter() {std::cout << "[";}
    ~ArrayWriter() noexcept {std::cout << "], ";}

    template <typename T>
    void operator()(const T& v) {std::cout << v << ", ";}
};

// "Regular" shared_ptr
template <typename T>
void serialize(Writer& writer, const std::shared_ptr<T> ptr)
{
    static_assert(!std::is_array_v<T>,
                  "shared_ptr<T[]> not supported: size unknowable");
    if (ptr)
        writer(*ptr);
    else
        writer("null");
}

// shared_ptr holding an array of known size
template <typename T, std::size_t N>
void serialize(Writer& writer, const std::shared_ptr<T[N]> ptr)
{
    if (ptr)
    {
        ArrayWriter arrayWriter;
        static constexpr auto size = N;
        for (std::size_t i=0; i<size; ++i) 
            arrayWriter(ptr[i]);
    }
    else
        writer("null");
}

int main()
{
    Writer writer;
    std::shared_ptr<std::string> s{new std::string{"Hello"}};
    std::shared_ptr<int[3]> n{new int[3]}; // Error prone!
    std::shared_ptr<float[]> x{new float[5]}; // Size lost
    n[0] = 1; n[1] = 2; n[2] = 3;
    serialize(writer, s); // Outputs Hello,
    serialize(writer, n); // Outputs [1, 2, 3, ],
    // serialize(writer, x); // static assertion failure

    return 0;
}

Working example: https://onlinegdb.com/r1R5jv0wI

sparik is still correct in his answer about not being able to know the size when given a shared_ptr<T> out of the blue (I should have worded my question better).

David Schwartz has pointed out in comments that the user may fail to initialize the array elements correctly (or use the wrong dynamic size) and my serializer would have no way of knowing. Even if smart pointers containing raw dynamic arrays can work in my case, it might not be a good idea to support them.

Thanks everyone for your comments and answers!


Addendum

According to this answer, unique_ptr<T[N]> is ill-formed, and indeed doesn't work for me in GCC.

I found P0674R1 (Extending make_shared to Support Arrays) and it provides several examples of shared_ptr<T[N]>, so it appears to be legal assuming that the committee didn't disallow it when it was adopted.

It's a bit annoying that unique_ptr and shared_ptr don't behave the same way in that respect.

Emile Cormier
  • 28,391
  • 15
  • 94
  • 122
  • Pretty bizarre situation. I was convinced that smart_ptr FAPP behaves the same as unique_ptr, only difference being the added ref count. Thx for pointing that out. Also, the std document is not very clear with what's really happening with U[] vs. U[N]. Somehow the smart_ptr implementation seems to know/access the size anyway. – non-user38741 Apr 12 '20 at 06:15