31

To improve efficiency of std::vector<T>, it's underlying array needs to be pre-allocated and sometimes re-allocated. That, however, requires the creation and later moving of objects of type T with a copy ctor or move ctor.

The problem that I am having is that T cannot be copied or moved because it contains objects that cannot be copied or moved (such as atomic and mutex). (And, yes, I am implementing a simple thread pool.)

I would like to avoid using pointers because:

  1. I do not need a level of indirection and so I do not want one.
  2. (Pointers are less efficient and increase complexity. Using pointers increases memory fragmentation and decreases data locality which can (but not necessarily must) cause a noticeable performance impact. Not so important, but still worth consideration.)

Is there a way to avoid a level of indirection here?

UPDATE: I fixed some incorrect assumptions and re-phrased the question, based on feedback in comments and answers.

Domi
  • 22,151
  • 15
  • 92
  • 122
  • If you can tolerate all of your thread pool objects being default constructed, offering instead other methods to make them live/dead other than ctor/dtor, would `std::array` do the trick? – Joe Z Dec 10 '13 at 06:43
  • @JoeZ Currently, my pool can vary in size (threads can be added to/removed from it). If there is no better solution, I can consider that and remove dynamic allocation features. – Domi Dec 10 '13 at 06:45
  • Well, the number of active threads may change, but the lifetimes of the threads don't perfectly nest. Because you cannot move the objects, your thread pool structure will have "gaps" in it. An array feels "right" if you don't want indirection on every action. You'll still probably end up with pointers threading together your active-thread list and your inactive-thread list. – Joe Z Dec 10 '13 at 06:47
  • 4
    I think for_each is OK for vector members as long as you're using reference to access the members. – Mine Dec 10 '13 at 06:50
  • how about vector::reserve or std::move? – UldisK Dec 10 '13 at 07:05
  • 2
    *pre*-allocation is *not* a problem. The memory is pre-allocated, but the objects are only constructed when inserted and it can be done directly in the array using `emplace`*. But *re*-allocation requires at least move ctor and you don't have one. – Jan Hudec Dec 10 '13 at 07:16
  • 1
    Regarding your edit: *"I do not need a level of indirection and so I do not want one."* Apparently you do, in fact, need a level of indirection, just not for the reasons you're thinking of. *"Using pointers increases memory fragmentation and decreases data locality ..."* Utter nonsense – this depends entirely on the allocation scheme. (You're probably thinking of out-of-the-box `new`, which is only incidentally related to pointers). – ildjarn Dec 11 '13 at 07:58
  • 1
    @ildjarn Not sure what you mean. I did learn that I need a level of indirection, but it is hidden in the `list` or `forward_list`, as I suggest in my answer. Of course, depending on when and how I allocate my objects, they can actually be stored in contiguous chunks, but I cannot *ensure* that, if I don't write my own allocator. Even if they are in a single chunk, adding a level of indirection is overhead, even though it is usual marginal. Furthermore, pointers always make the code more complex. Can you please point out exactly where I am wrong? – Domi Dec 11 '13 at 10:35

4 Answers4

21

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; } );
}
BЈовић
  • 62,405
  • 41
  • 173
  • 273
16

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.

Stephan Dollberg
  • 32,985
  • 16
  • 81
  • 107
  • The `deque` is good (just like `list` and `forward_list`) but one last complaint I have about this is that it needs a public ctor. I imagine there is no way around that? – Domi Dec 10 '13 at 08:02
  • @Domi, what do you mean by a "public ctor"? How else would you like to create elements without a public ctor? – Stephan Dollberg Dec 10 '13 at 08:02
  • Right now, I have the pool as a friend, so it can use a private ctor. But that require moving of things, so, I guess, there is no better way. – Domi Dec 10 '13 at 08:03
  • @Domi: you can have a `private` initial constructor with a `public` move constructor, move assignment operator and destructor. – Matthieu M. Dec 10 '13 at 08:23
  • @MatthieuM. I can't move these objects. – Domi Dec 10 '13 at 08:28
  • @Domi I don't see your problem here, if you need a public constructor, then just make it public. Anyway, I don't see how this is related to the actual question. – Stephan Dollberg Dec 10 '13 at 08:29
  • @bamboon It's not related. It is just another concern I have, but that is orthogonal to this specific problem. Sorry about that! – Domi Dec 10 '13 at 08:41
  • @bamboon However, `deque` does not support removal, as I laid out in my answer, which is why I had to find a different solution. – Domi Dec 10 '13 at 12:05
  • 1
    @Domi Yeah, sorry for that. My original answer had list instead of deque, when changing to deque, I forgot to check for removal. Since, I have updated the answer to include both deque and list. – Stephan Dollberg Dec 10 '13 at 12:27
1

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.

Community
  • 1
  • 1
Domi
  • 22,151
  • 15
  • 92
  • 122
  • 1
    You cannot use `operator+` on `std::list::iterator`. If you really want that linear-time operation, you have to use `std::next(it, n)` to return an iterator that is `n` steps ahead of `it`. – David Stone Aug 23 '14 at 16:04
  • In VS c++20 (not sure about others) you cannot use `forward_list` for this because both `remove` and `remove_if` use `move` to rearrange object after removal. So it looks like _"each cell is allocated independently"_ statement is not valid. – Oleg Korzhukov Sep 09 '22 at 08:05
  • @OlegKorzhukov Maybe it used to be like that, and they finally realized that `forward_list` is way slower sometimes, so they "fixed" the implementation? – Domi Sep 09 '22 at 09:33
0

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!

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
  • You are essentially saying: You can use `vector` just fine, as long as you don't use most of what makes `vector` `vector` :) – Domi Oct 14 '17 at 04:24
  • 1
    Sure, but most of the banned member functions _inherently_ involve copying or moving. As you've mentioned you can't copy or move the contained objects, so most of the functionality offered by vector (or by any contiguous and re-sizable array) simply isn't possible for your objects. So the limitation isn't so much related to `vector`, but more "what can you do with a dynamic array of objects that can't be moved or copied?". The remaining functionality is still quite useful: a fixed-but-dynamically-sized array-like container, with guaranteed destruction and contiguous storage. @Domi – BeeOnRope Oct 14 '17 at 05:53
  • If you want more even more dynamic behavior, e.g, the ability to add an arbitrary about of objects, rather than use a fixed size at construction, `std::deque` is a good bet. It allows you `emplace_back` objects even w/o move or copy constructors. You can remove even more limitations with `std::list` at the cost of performance. – BeeOnRope Oct 14 '17 at 05:55
  • It seems so! I just read the accepted answer and also the most upvoted answer which don't even mention it. I'm not sure why the accepts answer was chosen - the other two seem strictly better, particularly the one by inf. @Domi – BeeOnRope Oct 14 '17 at 13:42
  • If you check the timestamps on the conversation threads, you see that there was a feedback loop between our answers: It was not until after I posted my answer that `inf` mentioned the two lists. And he did not address the VS 2012 bug that caused me a lot of trouble when trying to implement it. (I have not checked since - one can only hope that this is fixed in the latest VS versions) – Domi Oct 14 '17 at 14:08