34

Is there a standard container for a sequence of fixed length, where that length is determined at runtime. Preferrably, I'd like to pass an argument to the constructor of each sequence element, and use that argument to initialize a const member (or a reference). I'd also like to obtain the sequence element at a given index in O(1). It seems to me that all of my requirements cannot be met at the same time.

  • I know std::array has fixed length, but that length has to be known at compile-time.
  • std::vector has dynamic size, and allows passing contructor arguments using emplace. Although you can reserve memory to avoid actual reallocations, the type still has to be movable to theoretically allow such reallocations, which e.g. prevents const members.
  • Then there is std::list and std::forward_list, which don't require a movable type, but which are still resizable and will perform rather poorly under random-access patterns. I also feel that there might be considerable overhead associated with such lists, since each list node will likely be allocated separately.
  • Strangely enough, std::valarray is my best bet so far, since it has a fixed length and won't resize automatically. Although there is a resize method, your type won't have to be movable unless you actually call that method. The main deficit here is the lack for custom constructor arguments, so initializing const members isn't possible with this approach.

Is there some alternative I missed? Is there some way to adjust one of the standard containers in such a way that it satisfies all of my requirements?


Edit: To give you a more precise idea of what I'm trying to do, see this example:

class A {
  void foo(unsigned n);
};

class B {
private:
  A* const a;
  const unsigned i;
public:
  B(A* aa) : a(aa), i(0) { }
  B(A* aa, unsigned ii) : a(aa), i(ii) { }
  B(const std::pair<A*, unsigned>& args) : B(args.first, args.second) { }
  B(const B&) = delete;
  B(B&&) = delete;
  B& operator=(const B&) = delete;
  B& operator=(B&&) = delete;
};

void A::foo(unsigned n) {
  // Solution using forward_list should be guaranteed to work
  std::forward_list<B> bs_list;
  for (unsigned i = n; i != 0; --i)
    bs_list.emplace_front(std::make_pair(this, i - 1));

  // Solution by Arne Mertz with single ctor argumen
  const std::vector<A*> ctor_args1(n, this);
  const std::vector<B> bs_vector(ctor_args1.begin(), ctor_args1.end());

  // Solution by Arne Mertz using intermediate creator objects
  std::vector<std::pair<A*, unsigned>> ctor_args2;
  ctor_args2.reserve(n);
  for (unsigned i = 0; i != n; ++i)
    ctor_args2.push_back(std::make_pair(this, i));
  const std::vector<B> bs_vector2(ctor_args2.begin(), ctor_args2.end());
}
Chiel
  • 6,006
  • 2
  • 32
  • 57
MvG
  • 57,380
  • 22
  • 148
  • 276
  • So you basically want a completely immutable container? – leftaroundabout Feb 15 '13 at 12:53
  • 1
    @leftaroundabout No, I suppose he just wants something like vector that never relocates its storage (and that property has to be *static*, i.e. known at compile-time). – R. Martinho Fernandes Feb 15 '13 at 12:54
  • I know that I once stored types in a `std::vector` that were not movable. This worked as long as I did not use `resize()`. However, I am not sure if this behavior is portable. – cschwan Feb 15 '13 at 12:55
  • @leftaroundabout: Yes, an immutable container with powerful construction facilities and proper destruction. The objects themselves will be mutable, but will have some const members so they are not freely mutable. The set of objects won't chnage over the lifetime of the container. – MvG Feb 15 '13 at 12:55
  • 2
    A vector will only relocate its storage if it grows. So if you know at runtime (early enough) how many objects you need to store, that shouldn't be a problem. – Mats Petersson Feb 15 '13 at 12:56
  • 4
    @MatsPetersson the code has to compile first. You cannot compile a call to push_back or to emplace_back without a movable type. (it does not matter if those paths are never hit; dead code still has to compile) – R. Martinho Fernandes Feb 15 '13 at 12:56
  • 1
    @cschwan, I'd ve *very* interested to see this working in a toy example. I can't see how the compiler can be sure at compile time that no resizing will be neccessary, unless it is performing very heavy optimization of a level I'd not have considered possible. – MvG Feb 15 '13 at 12:57
  • @MvG: Sorry, I think I have misremembered something - I wasnt able to write a working example. – cschwan Feb 15 '13 at 13:23
  • 2
    The requirements for `valarray` are hidden in section 26.2, and include copy constructor and assignment operator for type T. – Bo Persson Feb 15 '13 at 13:27
  • @BoPersson, do these requirements still apply if I don't call `resize`? So my code would be non-portable even if it works for me? That leaves linked lists as the only option. – MvG Feb 15 '13 at 14:00
  • @MvG - The elements stored in a valarray are supposed to behave like a value type, for example `int`, or `double`, or a user defined type that behaves similarly. Section 26.2 contains about half a page of detailed requirements, without excepting any specific functions. – Bo Persson Feb 15 '13 at 16:00

4 Answers4

10

Theoretically vector has the properties you need. As you noted, actions that possibly do assignments to the contained type, including especially any sequence modifications (empace_back, push_back, insert etc.) are not supported if the elements are noncopyable and/or nonassignable. So to create a vector of noncopyable elements, you'd have to construct each element during vector construction.

As Steve Jessop points out in his answer, if you define the vector const in the first place you won't even be able to call such modifying actions - and of course the elements remain unchanged as well.

If I understand correctly, you have only a sequence of constructor arguments, not the real object sequence. If it's only one argument and the contained type has a corresponding constructor, things shoule be easy:

struct C
{
  const int i_;  
  C(int i) : i_(i) {}
};

int main()
{
  const std::vector<C> theVector { 1, 2, 3, 42 };
}

If the constructor is explicit, you have to make a list first or explicitly construct the objects in the initializer-list:

int main()
{
  auto list = { 1, 2, 3, 4 };
  const std::vector<C> theVector (std::begin(list), std::end(list));
  const std::vector<C> anotherVector { C(1), C(44) };
}

If it's more than just one argument per constructed object, consider a intermediate creator object:

struct C
{
  const int i_;  
  C(int i, int y) : i_(i+y) {}
};

struct CCreator
{ 
  int i; int y; 
  explicit operator C() { return C(i,y); }
};

int main()
{
  const std::vector<CCreator> ctorArgs = { {1,2}, {3,42} };
  const std::vector<C> theVector { begin(ctorArgs), end(ctorArgs) };
}
Arne Mertz
  • 24,171
  • 3
  • 51
  • 90
  • This seems to not really be an answer to the question, but AFAICS it fulfills all the requirements (works even when all the special member functions are explicitly deleted), and doesn't have any caveats such as performance overhead. +1. – leftaroundabout Feb 15 '13 at 13:40
  • 1
    Well it kind of is an answer to both parts of the question: **1** He did not miss an alternative, since vector fits. **2** He can "adjust" one of the standard containers by not using the wrong methods like emplace_back etc. ;-) – Arne Mertz Feb 15 '13 at 13:46
  • Static initializer lists are not applicable in my case, as the length will only be known at runtime. However the idea of using the iterator pair version of the `vector` constructor works well for me, since that one does automatic conversion. Updated my question with example code. One problem is that I have to have a separate `vector` for the constructor arguments, which will remain in scope long after I'm done using it, so I'll likely construct some function aroud that which returns the finished vector. Or custom sequence iterators. If only C++11 had ranges… – MvG Feb 15 '13 at 14:21
6

I think const std::vector<T> has the properties you ask for. Its elements aren't actually defined with const, but it provides a const view of them. You can't change the size. You can't call any of the member functions that need T to be movable, so for normal use they won't be instantiated (they would be if you did an extern class declaration, so you can't do that).

If I'm wrong, and you do have trouble because T isn't movable, try a const std::deque<T> instead.

The difficulty is constructing the blighter -- in C++11 you can do this with an initializer list, or in C++03 you can construct a const vector from a non-const vector or from anything else you can get iterators for. This doesn't necessarily mean T needs to be copyable, but there does need to be a type from which it can be constructed (perhaps one you invent for the purpose) .

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • The *anything else you can get iterators for* part is particularly useful. Arne Mertz provided more detail on this, but the core idea is there in both your answers, and I believe it can be made to work nicely once I hide that initialization uglyness somewhere behind the scenes. – MvG Feb 15 '13 at 14:40
3

Add a level of indirection by using a std::shared_ptr. The shared pointer can be copied and assigned as usual, but without modifying the object that is pointed to. This way you should not have any problems, as the following example shows:

class a
{
public:
    a(int b) : b(b) { }

    // delete assignment operator
     a& operator=(a const&) = delete;

private:
    // const member
    const int b;
};

// main
std::vector<std::shared_ptr<a>> container;

container.reserve(10);
container.push_back(std::make_shared<a>(0));
container.push_back(std::make_shared<a>(1));
container.push_back(std::make_shared<a>(2));
container.push_back(std::make_shared<a>(3));

Another advantage is the function std::make_shared which allows you to create your objects with an arbitrary number of arguments.


Edit:

As remarked by MvG, one can also use std::unique_ptr. Using boost::indirect_iterator the indirection can be removed by copying the elements into a new vector:

void A::foo(unsigned n)
{
    std::vector<std::unique_ptr<B>> bs_vector;
    bs_vector.reserve(n);

    for (unsigned i = 0; i != n; ++i)
    {
        bs_vector.push_back(std::unique_ptr<B>(new B(this, i)));
    }

    typedef boost::indirect_iterator<std::vector<std::unique_ptr<B>>::iterator> it;

    // needs copy ctor for B
    const std::vector<B> bs_vector2(it(bs_vector.begin()), it(bs_vector.end()));

    // work with bs_vector2
}
cschwan
  • 3,283
  • 3
  • 22
  • 32
  • 1
    `shared_ptr` feels like a lot of overhead here. I guess `unique_ptr` should suffice, since I don't need things to be copyable, just movable. Memory management overhead should be similar to list-based solutions, but access is O(1), so definitely a win there. One problem with this is that you have to add one level of dereferencing on every access, so this isn't a drop-in replacement for object-valued containers, but instead requires modifications to all the code accessing that container. – MvG Feb 15 '13 at 14:44
  • MvG: I edited my answer and included some code on how to remove the indirection. – cschwan Feb 15 '13 at 15:21
  • MvG: Why do you need a const member at all? – cschwan Feb 15 '13 at 15:25
  • The constness is just there to express the contract that this relationship between objects should never change. Your copy of the vector feels bad, since it does indeed require a copy ctor. But the use of `boost::indirect_iterator` might be useful if I were to replace `vector` with something other that builds on it and provides these indirect iterators to access the elements. – MvG Feb 15 '13 at 15:32
-1

I also encounter this problem, the use case in my code is to provide a thread-safe vector, the elements number is fixed and are atomic numbers. I have read all the great answers here. I think we may also consider my solution:

Just inherited the std::vector and hide the modifiers such as push_back, emplace_back, erase, then we get a fixed size vector. We can only access and modify the elements with operator [].

template <typename T>
class FixedVector : protected std::vector<T> {
 public:
  using BaseType = std::vector<T>;
  FixedVector(size_t n) : BaseType(n) {}
  FixedVector(const T &val, size_t n) : BaseType(val, n) {}
  typename BaseType::reference operator[](size_t n) {
    return BaseType::operator[](n);
  }
};
prehistoricpenguin
  • 6,130
  • 3
  • 25
  • 42