简体   繁体   中英

How to fill in a C++ container with fixed-size from vector with size defined at run-time?

Background: this is a followup to @pari's answer to Constant-sized vector .

My type, metadata_t does not have a default constructor.

I use std::make_unique for fixed-size arrays that the size is not available in compile-time. (ie const size).

typedef std::unique_ptr<metadata_t[]> fixedsize_metadata_t;
fixedsize_metadata_t consolidate(const std::vector<metadata_t> &array) {

    // note: this is run-time:
    auto n = array.size();

    return fixedsize_side_metadata_t(array.begin(), array.end());  // error
    return fixedsize_side_metadata_t(array);  // error
    return std::unique_ptr<metadata_t[]>(0); // no error, but not useful
    return std::unique_ptr<metadata_t[]>(n); // error
}

However, the constructor of unique_ptr<...[]> only accepts a size (integer). How do I initialise and copy my vector into my unique_ptr<[]> ?

I tried std::unique_ptr<metadata_t[]>(array.size()); to prepare and then copy/populate the contents in the next step, but it shows a compile error.

Note: I use C++20 (or higher if it was available). Could make_unique_for_overwrite be useful? ( C++23 ).

Note: At first I thought generate (as in this answer ) can do it, but it does not solve my problem because n is a run-time information.

The size of my vector is determined at run-time.

The whole point of this function is to convert my std::vector into a fixed-size data structure (with run-time size).

The data structure does not have to be unique_ptr<T[]> . The old title referred to unique_ptr but I am really looking for a solution for a fixed-size data structure. So far, it is the only data structure I found as a "constant-size indexed container with size defined at runtime".

You can't initialize the elements of a unique_ptr<T[]> array with the elements of a vector<T> array when constructing the new array (UPDATE: apparently you can , but it is still not going to be a solution solved with a single statement, as you are trying to do).

You will have to allocate the T[] array first, and then copy the vector 's elements into that array one at a time, eg:

typedef std::unique_ptr<metadata_t[]> fixedsize_metadata_t;

fixedsize_metadata_t consolidate(const std::vector<metadata_t> &array) {
    fixedsize_metadata_t result = std::make_unique<metadata_t[]>(array.size());
    std::copy(array.begin(), array.end(), result.get());
    return result;
}

UPDATE: you updated your question to say that metadata_t does not have a default constructor. Well, that greatly complicates your situation.

The only way to create an array of objects that don't support default construction is to allocate an array of raw bytes of sufficient size and then use placement-new to construct the individual objects within those bytes. But now, you are having to manage not only the objects in the array, but also the byte array itself. By itself, unique_ptr<T[]> won't be able to free that byte array, so you would have to provide it with a custom deleter that frees the objects and the byte array. Which also means, you will have to keep track of how many objects are in the array (something new[] does for you so delete[] works, but you can't access that counter, so you will need your own), eg:

struct metadata_arr_deleter
{
    void operator()(metadata_t *arr){
        size_t count = *(reinterpret_cast<size_t*>(arr)-1);
        for (size_t i = 0; i < count; ++i) {
            arr[i]->~metadata_t();
        }
        delete[] reinterpret_cast<char*>(arr);
    }
};

typedef std::unique_ptr<metadata_t[], metadata_arr_deleter> fixedsize_metadata_t;

fixedsize_metadata_t consolidate(const std::vector<metadata_t> &array) {

    const auto n = array.size();
    const size_t element_size = sizeof(std::aligned_storage_t<sizeof(metadata_t), alignof(metadata_t)>);

    auto raw_bytes = std::make_unique<char[]>(sizeof(size_t) + (n * element_size));

    size_t *ptr = reinterpret_cast<size_t*>(raw_bytes.get());
    *ptr++ = n;
    auto *uarr = reinterpret_cast<metadata_t*>(ptr);
    size_t i = 0;

    try {
        for (i = 0; i < n; ++i) {
            new (&uarr[i]) metadata_t(array[i]);
        }
    }
    catch (...) {
        for (size_t j = 0; j < i; ++j) {
            uarr[j]->~metadata_t();
        }
        throw;
    }

    raw_bytes.release();
    return fixedsize_metadata_t(uarr);
}

Needless to say, this puts much more responsibility on you to allocate and free memory correctly, and it is really just not worth the effort at this point. std::vector already supports everything you need. It can create an object array using a size known at runtime, and it can create non-default-constructable objects in that array, eg.

std::vector<metadata_t> consolidate(const std::vector<metadata_t> &array) {

    auto n = array.size();

    std::vector<metadata_t> varr;
    varr.reserve(n);

    for (const auto &elem : array) {
        // either:
        varr.push_back(elem);
        // or:
        varr.emplace_back(elem);
        // it doesn't really matter in this case, since they
        // will both copy-construct the new element in the array
        // from the current element being iterated...
    }

    return varr;
}

Which is really just a less-efficient way of avoiding the vector 's own copy constructor:

std::vector<metadata_t> consolidate(const std::vector<metadata_t> &array) {
    return array; // will return a new copy of the array
}

The data structure does not have to be unique_ptr<T[]> . The old title referred to unique_ptr but I am really looking for a solution for a fixed-size data structure. So far, it is the only data structure I found as a "constant-size indexed container with size defined at runtime".

What you are looking for is exactly what std::vector already gives you. You just don't seem to realize it, or want to accept it. Both std::unique_ptr<T[]> and std::vector<T> hold a pointer to a dynamically-allocated array of a fixed size specified at runtime. It is just that std::vector offers more functionality than std::unique_ptr<T[]> does to manage that array (for instance, re-allocating the array to a different size). You don't have to use that extra functionality if you don't need it, but its base functionality will suit your needs just fine.

Initializing an array of non-default constructibles from a vector is tricky.

One way, if you know that your vector will never contain more than a certain amount of elements, could be to create an index_sequence covering all elements in the vector . There will be one instantiation of the template for each number of elements in your vector that you plan to support and the compilation time will be "silly".

Here I've selected the limit 512. It must have a limit, or else the compiler will spin in endless recursion until it gives up or crashes.

namespace detail {
template <class T, size_t... I>
auto helper(const std::vector<T>& v, std::index_sequence<I...>) {
    if constexpr (sizeof...(I) > 512) { // max 512 elements in the vector.
        return std::unique_ptr<T[]>{};  // return empty unique_ptr or throw
    } else {
        // some shortcuts to limit the depth of the call stack
        if(sizeof...(I)+255 < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+256>{});
        if(sizeof...(I)+127 < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+128>{});
        if(sizeof...(I)+63 < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+64>{});
        if(sizeof...(I)+31 < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+32>{});
        if(sizeof...(I)+15 < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+16>{});
        if(sizeof...(I)+7 < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+8>{});
        if(sizeof...(I)+3 < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+4>{});
        if(sizeof...(I)+1 < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+2>{});
        if(sizeof...(I) < v.size())
            return helper(v, std::make_index_sequence<sizeof...(I)+1>{});

        // sizeof...(I) == v.size(), create the pointer:
        return std::unique_ptr<T[]>(new T[sizeof...(I)]{v[I]...});
    }
}
} // namespace detail

template <class T>
auto make_unique_from_vector(const std::vector<T>& v) {
    return detail::helper(v, std::make_index_sequence<0>{});
}

You can then turn your vector into a std::unique_ptr<metadata_t[]> :

auto up = make_unique_from_vector(foos);
if(up) {
    // all is good
}

Demo (compilation time may exceed the time limit)

You have to allocate some uninitialized memory for an array and copy construct the elements in-place using construct_at . You can then create a unique_ptr using the address of the constructed array:

#include <vector>
#include <memory>

struct metadata_t {
    metadata_t(int) { }
};

typedef std::unique_ptr<metadata_t[]> fixedsize_metadata_t;

fixedsize_metadata_t consolidate(const std::vector<metadata_t> &array) {
    // note: this is run-time:
    auto n = array.size();
    std::allocator<metadata_t> alloc;
    metadata_t *t = alloc.allocate(n);
    for (std::size_t i = 0; i < array.size(); ++i) {
        std::construct_at(&t[i], array[i]);
    }
    return fixedsize_metadata_t(t);
}

You can allocate raw memory, copy (or move) construct your data there, and store the result in a unique_ptr. I'm not dealing with exception safety if your copy constructor throws.

metadata_t* storage = static_cast<metadata_t*>(malloc(array.size() * sizeof(metadata_t)));
for (size_t ii = 0; ii < array.size(); ++ii)
    new (&storage[ii]) metadata_t(array[ii]); // copy construct
return std::unique_ptr<metadata_t[]>(storage);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM