Mikkel Rev
Mikkel Rev

Reputation: 901

Can we use conventional pointer arithmetic with std::array?

I want to work out how to use old style pointer arithmetic on pointers to elements of the std::array class. The following code (unsurprisingly perhaps) does not compile:

int main(int argc, char *argv[])
{
    double* data1 = new double[(int)std::pow(2,20)];
    std::cout << *data1 << " " << *(data1 +1) << std::endl;
    delete data1;
    data1 = NULL;

    double* data2 = new std::array<double, (int)std::pow(2,20)>;
    std::cout << *data2 << " " << *(data2 +1) << std::endl;
    delete data2;
    data2 = NULL;

    return 0;
}

As an exercise, I want to use all the conventional pointer arithmetic, but instead of pointing at an old style double array, I want it to point to the elements of a std::array. My thinking with this line:

    double* data2 = new std::array<double, (int)std::pow(2,20)>;

is to instruct the compiler that data2 is a pointer to the first element of the heap allocated std::array<double,(int)std::pow(2,20)>.

I have been taught that the double* name = new double[size]; means EXACTLY the following: «Stack allocate memory for a pointer to ONE double and name the pointer name, then heap allocate an array of doubles of size size, then set the pointer to point to the first element of the array.» Since the above code does not compile, I must have been taught something incorrect since the same syntax doesnt work for std::arrays.

This raises a couple of questions:

  1. What is the actual meaning of the statement type* name = new othertype[size];?
  2. How can I achieve what I want using std::array?
  3. Finally, how can I achieve the same using std::unqiue_ptr and std::make_unique?

Upvotes: 0

Views: 2744

Answers (6)

bipll
bipll

Reputation: 11940

std::array is a STL container after all!

auto storage = std::array<double, 1 << 20>{};
auto data = storage.begin();
std::cout << *data << " " << *(data + 1) << std::endl;

Upvotes: 1

einpoklum
einpoklum

Reputation: 132118

Can we use conventional pointer arithmetic with std::array?

Yes, sure you can - but not on the array itself, which is an object. Rather, you use the address of the data within the array, which you get with the std::array's data() method, like so:

std::array<double, 2>  data2 { 12.3, 45.6 };
double* raw_data2 = data2.data(); // or &(*data2.begin());
std::cout << *raw_data2 << " " << *(raw_data2 + 1) << std::endl;

and this compiles and runs fine. But you probably don't really need to use pointer arithmetic and could just write your code different, utilizing the nicer abstraction of an std::array.

PS - Avoid using explicit memory allocation with new and delete(see the C++ Core Guidelines item about this issue). In your case you don't need heap allocation at all - just like you don't need it with the regular array.

Upvotes: 4

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275820

Sure, these are all legal:

template<class T, std::size_t N>
T* alloc_array_as_ptr() {
  auto* arr = new std::array<T,N>;
  if (!arr) return nullptr;
  return arr->data();
}
template<class T, std::size_t N>
T* placement_array_as_ptr( void* ptr ) {
  auto* arr = ::new(ptr) std::array<T,N>;
  return arr->data();
}
template<std::size_t N, class T>
std::array<T, N>* ptr_as_array( T* in ) {
  if (!in) return nullptr;
  return reinterpret_cast<std::array<T,N>*>(in); // legal if created with either above 2 functions!
}
// does not delete!
template<std::size_t N, class T>
void destroy_array_as_ptr( T* t ) {
  if (!t) return;
  ptr_as_array<N>(t)->~std::array<T,N>();
}
// deletes
template<std::size_t N, class T>
void delete_array_as_ptr(T* t) {
  delete ptr_as_array<N>(t);
}

the above is, shockingly, actually legal if used perfectly. The pointer-to-first-element-of-array is pointer interconvertable with the entire std::array.

You do have to keep track of the array size yourself.

I wouldn't advise doing this.

Upvotes: 1

tadman
tadman

Reputation: 211690

The meaning of a generalized:

type* name = new othertype[size];

Ends up being "I need a variable name that's a pointer to type and initialize that with a contiguous allocation of size instances of othertype using new[]". Note that this involves casting and might not even work as othertype and type might not support that operation. A std::array of double is not equivalent to a pointer to double. It's a pointer to a std::array, period, but if you want to pretend that's a double and you don't mind if your program crashes due to undefined behaviour you can proceed. Your compiler should warn you here, and if it doesn't your warnings aren't strict enough.

Standard Library containers are all about iterators, not pointers, and especially not pointer arithmetic. Iterators are far more flexible and capable than pointers, they can handle exotic data structures like linked lists, trees and more without imposing a lot of overhead on the caller.

Some containers like std::vector and std::array support "random access iterators" which are a form of direct pointer-like access to their contents: a[1] and so on. Read the documentation of any given container carefully as some permit this, and many don't.

Remember that "variable" and "allocated on stack" are not necessarily the same thing. An optimizing compiler can and will put that pointer wherever it wants, including registers instead of memory, or nowhere at all if it thinks it can get away with it without breaking the expressed behaviour of your code.

If you want std::array, which you really do as Standard Library containers are almost always better than C-style arrays:

std::array<double, 2> data2;

If you need to share this structure you'll need to consider if the expense of using std::unique_ptr is worth it. The memory footprint of this thing will be tiny and copying it will be trivial, so it's pointless to engage a relatively expensive memory management function.

If you're passing around a larger structure, consider using a reference instead and locate the structure in the most central data structure you have so it doesn't need to be copied by design.

Upvotes: 1

Max Langhof
Max Langhof

Reputation: 23701

You can have access to the "raw pointer" view of std::array using the data() member function. However, the point of std::array is that you don't have to do this:

int main(int argc, char *argv[])
{
    std::array<double, 2> myArray;
    double* data = myArray.data();
    // Note that the builtin a[b] operator is exactly the same as
    // doing *(a+b).
    // But you can just use the overloaded operator[] of std::array.
    // All of these thus print the same thing:
    std::cout << *(data) << " " << *(data+1) << std::endl;
    std::cout << data[0] << " " << data[1] << std::endl;
    std::cout << myArray[0] << " " << myArray[1] << std::endl;

    return 0;
}

Upvotes: 3

Brian Bi
Brian Bi

Reputation: 119467

I have been taught that the double* name = new double[size]; means EXACTLY the following: «Stack allocate memory for a pointer to ONE double and name the pointer name, then heap allocate an array of doubles of size size, then set the pointer to point to the first element of the array.» Since the above code does not compile, I must have been taught something incorrect since the same syntax doesnt work for std::arrays.

You are correct about that statement, but keep in mind that the way this works is that new[] is a different operator from new. When you dynamically allocate an std::array, you're calling the single-object new, and the returned pointer points to the std::array object itself.

You can do pointer arithmetic on the contents of an std::array. For example, data2.data() + 1 is a pointer to data2[1]. Note that you have to call .data() to get a pointer to the underlying array.

Anyway, don't dynamically allocate std::array objects. Avoid dynamic allocation if possible, but if you need it, then use std::vector.

Upvotes: 7

Related Questions