How to store objects without copy or move constructor in std::vector?

前端 未结 4 465
情书的邮戳
情书的邮戳 2021-02-01 16:58

To improve efficiency of std::vector, it\'s underlying array needs to be pre-allocated and sometimes re-allocated. That, however, requires the creation and

相关标签:
4条回答
  • 2021-02-01 17:11

    You can store elements without move or copy constructors in std::vector, but you simply have to avoid methods which require the elements to have move or copy constructors. Almost1 anything that changes the size of the vector (e.g., push_back, resize(), etc).

    In practice, this means you need to allocate a fixed-size vector at construction time, which will invoke the default constructor of your objects, which you can modify with assignment. This could at least work for std::atomic<> objects, which can be assigned to.


    1 clear() is the only example of a size-changing method that doesn't require copy/move constructors, since it never needs to move or copy any elements (after all, the vector is empty after this operation). Of course, you can never again grow your zero-sized vector again after calling this!

    0 讨论(0)
  • 2021-02-01 17:13

    For the start, std::mutex can not be copied or moved, therefore you are forced to use some kind of indirection.

    Since you want to store mutex in a vector, and not copy it, I would use std::unique_ptr.

    vector<unique_ptr<T>> does not allow certain vector operations (such as for_each)

    I am not sure I understand that sentence. It is perfectly possible to do range for :

    std::vector< std::unique_ptr< int > > v;
    // fill in the vector
    for ( auto & it : v )
      std::cout << *it << std::endl;
    

    or to use std algorithms :

    #include <iostream>
    #include <typeinfo>
    #include <vector>
    #include <memory>
    #include <algorithm>
    
    
    int main()
    {
        std::vector< std::unique_ptr< int > > v;
        v.emplace_back( new int( 3 ) );
        v.emplace_back( new int( 5 ) );
        std::for_each( v.begin(), v.end(), []( const std::unique_ptr< int > & it ){ std::cout << *it << std::endl; } );
    }
    
    0 讨论(0)
  • 2021-02-01 17:20

    That requires however the creation of objects of type T with a copy ctor.

    That is not entirely right, as of C++11, if you use the constructor of std::vector which will default construct a number of elements, then you don't need to have a copy or move constructor.

    As such, if no threads are added or deleted from your pool, you can then just do:

    int num = 23;
    std::vector<std::mutex> vec(num);
    

    If you want to add or delete things dynamically, then you have to use an indirection.

    1. Use std::vector + std::unique_ptr as already proposed
    2. Use a std::deque, that allows you to neatly use it with range based for loops or std-algorithms and avoids all indirections. (Which only allows additions)
    3. Use a std::list/forward_list this solution is similar to number one, however it has the additional benefit of easier usage with range based for and algorithms. It's probably the best if you are only accessing the elements sequentially as there is no support for random-access.

    Like this:

    std::deque<std::mutex> deq;
    deq.emplace_back();
    deq.emplace_back();
    
    for(auto& m : deq) {
        m.lock();
    }
    

    As a final note, std::thread is of course moveable, so you can use std::vector + std::vector::emplace_back with it.

    0 讨论(0)
  • 2021-02-01 17:31

    To summarize what was proposed so far:

    1. Use vector<unique_ptr<T>> -- Adds an explicit level of indirection and was unwanted by OP.
    2. Use deque<T> -- I first tried deque, but erasing objects from it also does not work. See this discussion on differences between deque and list.

    The solution is to use forward_list which is a singly-linked list (or you can use list if you want a doubly-linked list). As @JanHudec pointed out, vector (and many of it's friends) require re-allocation when adding or removing items. That does not sit well with objects like mutex and atomic which are not allowed to be copied nor moved. forward_list and list do not require that because each cell is allocated independently (I cannot cite the standard on that, but the indexing method gives rise to that assumption). Since they are actually linked lists, they do not support random access indexing. myList.begin() + i will get you an iterator of the i'th element, but it (most certainly) will have to loop through all previous i cells first.

    I have not looked at the promises by the standard, but things work fine on Windows (Visual Studio) and CompileOnline (g++). Feel free to play around with the following test case on CompileOnline:

    #include <forward_list>
    #include <iostream>
    #include <mutex>
    #include <algorithm>
    
    using namespace std;
    
    class X
    {
        /// Private copy constructor.
        X(const X&);
        /// Private assignment operator.
        X& operator=(const X&);
    
    public:
        /// Some integer value
        int val;
        /// An object that can be neither copied nor moved
        mutex m;
    
        X(int val) : val(val) { }
    };
    
    
    int main()
    {
        // create list
        forward_list<X> xList;
    
        // add some items to list
        for (int i = 0; i < 4; ++i)
           xList.emplace_front(i);
    
        // print items
        for (auto& x : xList)
           cout << x.val << endl;
    
        // remove element with val == 1    
        // WARNING: Must not use remove here (see explanation below)
        xList.remove_if([](const X& x) { return x.val == 1; });
    
    
        cout << endl << "Removed '1'..." << endl << endl;
    
        for (auto& x : xList)
           cout << x.val << endl;
    
       return 0;
    }
    

    Output:

    Executing the program....
    $demo 
    3
    2
    1
    0
    
    Removed '1'...
    
    3
    2
    0
    

    I expect this to roughly have the same performance as vector<unique_ptr<T>> (as long as you don't use random access indexing too often).

    WARNING: Using forward_list::remove does not currently work in VS 2012. That is because it copies the element before trying to remove it. The header file Microsoft Visual Studio 11.0\VC\include\forward_list (same problem in list) reveals:

    void remove(const _Ty& _Val_arg)
    {   // erase each element matching _Val
        const _Ty _Val = _Val_arg;  // in case it's removed along the way
    
        // ...
    }
    

    So, it is copied "in case it's removed along the way". This means that list and forward_list don't even allow for storing unique_ptr. I assume that this is a design bug.

    The work-around is simple: You have to use remove_if instead of remove because the implementation of that function does not copy anything.

    A lot of the credit goes to the other answers. However, since none of them was a complete solution without pointers, I decided to write this answer.

    0 讨论(0)
提交回复
热议问题