Передача в функцию std :: array неизвестного размера

102

В C ++ 11, как мне написать функцию (или метод), которая принимает std :: array известного типа, но неизвестного размера?

// made up example
void mulArray(std::array<int, ?>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

// lets imagine these being full of numbers
std::array<int, 17> arr1;
std::array<int, 6>  arr2;
std::array<int, 95> arr3;

mulArray(arr1, 3);
mulArray(arr2, 5);
mulArray(arr3, 2);

Во время поиска я нашел только предложения по использованию шаблонов, но они кажутся беспорядочными (определения методов в заголовке) и чрезмерны для того, что я пытаюсь выполнить.

Есть ли простой способ заставить эту работу работать, как с обычными массивами в стиле C?

Адриан
источник
1
Массивы не имеют проверки границ и не знают, какого они размера. Поэтому вы должны во что-то их завернуть или подумать об использовании std::vector.
Travis Pessetto
20
Если шаблоны кажутся вам беспорядочными и чрезмерными, вам следует избавиться от этого чувства. Они обычное дело в C ++.
Benjamin Lindley
Есть ли причина не использовать, std::vectorкак рекомендует @TravisPessetto?
Cory Klein
2
Понятно. Если это ограничение их природы, мне придется с этим смириться. Причина, по которой я подумал об избежании std :: vector (который отлично работает для меня), заключается в том, что он размещен в куче. Поскольку эти массивы будут крошечными и будут проходить в цикле на каждой итерации программы, я подумал, что std :: array может работать немного лучше. Думаю, тогда я воспользуюсь массивом в стиле C, моя программа не сложная.
Адриан
15
@ Адриан Твой взгляд на производительность совершенно неверен. Не пытайтесь делать микрооптимизации, пока у вас еще не будет работоспособной программы. И после того, как у вас есть программа, не гадайте, что следует оптимизировать, вместо этого позвольте профилировщику сказать вам, какая часть программы должна быть оптимизирована.
Пол Манта

Ответы:

90

Есть ли простой способ заставить эту работу работать, как с обычными массивами в стиле C?

Нет. Вы действительно не сможете этого сделать, если не сделаете свою функцию шаблоном функции (или не используете другой вид контейнера, например std::vector, как предлагается в комментариях к вопросу):

template<std::size_t SIZE>
void mulArray(std::array<int, SIZE>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

Вот живой пример .

Энди Проул
источник
9
OP спрашивает, есть ли другое решение, кроме шаблонов.
Novak
1
@Adrian: К сожалению, нет другого решения, если вы хотите, чтобы ваша функция работала в общем на массивах любого размера ...
Энди Проул
1
Правильно: другого выхода нет. Поскольку каждый std :: array с разным размером относится к разному типу, вам нужно написать функцию, которая может работать с разными типами. Следовательно, шаблоны - это решение для std :: array.
bstamour
4
Самое прекрасное в использовании шаблона здесь заключается в том, что вы можете сделать его еще более универсальным, чтобы он работал с любым контейнером последовательности, а также со стандартными массивами:template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
Бенджамин Линдли
2
@AndyProwl Ссылка на живой пример больше не работает
Ахмед Хусейн,
28

Размер - arrayэто часть шрифта , поэтому вы не можете делать то, что хотите. Есть пара альтернатив.

Предпочтительнее было бы взять пару итераторов:

template <typename Iter>
void mulArray(Iter first, Iter last, const int multiplier) {
    for(; first != last; ++first) {
        *first *= multiplier;
    }
}

В качестве альтернативы используйте vectorвместо массива, что позволяет сохранять размер во время выполнения, а не как часть его типа:

void mulArray(std::vector<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}
Марк Б
источник
1
Я думаю, что это лучшее решение; если вы собираетесь создать шаблон, сделайте его полностью универсальным с помощью итераторов, которые позволят вам использовать любой контейнер (массив, список, вектор, даже указатели старой школы C и т.д.) без каких-либо недостатков. Спасибо за подсказку.
Марк Лаката
9

РЕДАКТИРОВАТЬ

C ++ 20 предварительно включает std::span

https://en.cppreference.com/w/cpp/container/span

Оригинальный ответ

Вам нужно что-то вроде того gsl::span, что доступно в библиотеке поддержки рекомендаций, описанной в основных рекомендациях C ++:

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#SS-views

Вы можете найти реализацию GSL с открытым исходным кодом только для заголовков здесь:

https://github.com/Microsoft/GSL

С помощью gsl::spanвы можете сделать это:

// made up example
void mulArray(gsl::span<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

// lets imagine these being full of numbers
std::array<int, 17> arr1;
std::array<int, 6>  arr2;
std::array<int, 95> arr3;

mulArray(arr1, 3);
mulArray(arr2, 5);
mulArray(arr3, 2);

Проблема в std::arrayтом, что его размер является частью его типа, поэтому вам придется использовать шаблон, чтобы реализовать функцию, которая принимает std::arrayпроизвольный размер.

gsl::spanс другой стороны, сохраняет свой размер как информацию времени выполнения. Это позволяет использовать одну функцию, не являющуюся шаблоном, для приема массива произвольного размера. Он также будет принимать другие смежные контейнеры:

std::vector<int> vec = {1, 2, 3, 4};
int carr[] = {5, 6, 7, 8};

mulArray(vec, 6);
mulArray(carr, 7);

Довольно круто, да?

Сунчо
источник
6

Я пробовал ниже, и у меня это сработало.

#include <iostream>
#include <array>

using namespace std;

// made up example
void mulArray(auto &arr, const int multiplier) 
{
    for(auto& e : arr) 
    {
        e *= multiplier;
    }
}

void dispArray(auto &arr)
{
    for(auto& e : arr) 
    {
        std::cout << e << " ";
    }
    std::cout << endl;
}

int main()
{

    // lets imagine these being full of numbers
    std::array<int, 7> arr1 = {1, 2, 3, 4, 5, 6, 7};
    std::array<int, 6> arr2 = {2, 4, 6, 8, 10, 12};
    std::array<int, 9> arr3 = {1, 1, 1, 1, 1, 1, 1, 1, 1};

    dispArray(arr1);
    dispArray(arr2);
    dispArray(arr3);

    mulArray(arr1, 3);
    mulArray(arr2, 5);
    mulArray(arr3, 2);

    dispArray(arr1);
    dispArray(arr2);
    dispArray(arr3);

    return 0;
}

ВЫХОД :

1 2 3 4 5 6 7

2 4 6 8 10 12

1 1 1 1 1 1 1 1 1

3 6 9 12 15 18 21

10 20 30 40 50 60

2 2 2 2 2 2 2 2 2

мускус
источник
3
Это не действительный C ++, а скорее расширение. Эти функции являются шаблонами, даже без template.
HolyBlackCat
1
Я изучил это, и оказалось, что auto foo(auto bar) { return bar * 2; }в настоящее время C ++ недействителен, хотя он компилируется в GCC7 с установленным флагом C ++ 17. После прочтения здесь параметры функции, объявленные как auto, являются частью Concepts TS, который в конечном итоге должен стать частью C ++ 20.
Fibbles
Предупреждение C26485
Metablaster
3

Безусловно, в C ++ 11 есть простой способ написать функцию, которая принимает std :: array известного типа, но неизвестного размера.

Если мы не можем передать размер массива функции, то вместо этого мы можем передать адрес памяти того места, где массив начинается, вместе со вторым адресом того места, где массив заканчивается. Позже, внутри функции, мы можем использовать эти 2 адреса памяти для вычисления размера массива!

#include <iostream>
#include <array>

// The function that can take a std::array of any size!
void mulArray(int* piStart, int* piLast, int multiplier){

     // Calculate the size of the array (how many values it holds)
     unsigned int uiArraySize = piLast - piStart;

     // print each value held in the array
     for (unsigned int uiCount = 0; uiCount < uiArraySize; uiCount++)     
          std::cout << *(piStart + uiCount) * multiplier << std::endl;
}

int main(){   

     // initialize an array that can can hold 5 values
     std::array<int, 5> iValues;

     iValues[0] = 5;
     iValues[1] = 10;
     iValues[2] = 1;
     iValues[3] = 2;
     iValues[4] = 4;

     // Provide a pointer to both the beginning and end addresses of 
     // the array.
     mulArray(iValues.begin(), iValues.end(), 2);

     return 0;
}

Вывод на консоль: 10, 20, 2, 4, 8

Дэвид М. Хельмут
источник
1

Это можно сделать, но чтобы все было чисто, нужно выполнить несколько шагов. Во-первых, напишите a template class, представляющий диапазон смежных значений. Затем перенаправьте templateверсию, которая знает, насколько велика arrayэта Implверсия, в версию, которая занимает этот непрерывный диапазон.

Наконец, реализуйте contig_rangeверсию. Обратите внимание , что for( int& x: range )работает contig_range, потому что я реализовал begin()и end()и указатели итераторы.

template<typename T>
struct contig_range {
  T* _begin, _end;
  contig_range( T* b, T* e ):_begin(b), _end(e) {}
  T const* begin() const { return _begin; }
  T const* end() const { return _end; }
  T* begin() { return _begin; }
  T* end() { return _end; }
  contig_range( contig_range const& ) = default;
  contig_range( contig_range && ) = default;
  contig_range():_begin(nullptr), _end(nullptr) {}

  // maybe block `operator=`?  contig_range follows reference semantics
  // and there really isn't a run time safe `operator=` for reference semantics on
  // a range when the RHS is of unknown width...
  // I guess I could make it follow pointer semantics and rebase?  Dunno
  // this being tricky, I am tempted to =delete operator=

  template<typename T, std::size_t N>
  contig_range( std::array<T, N>& arr ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
  template<typename T, std::size_t N>
  contig_range( T(&arr)[N] ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
  template<typename T, typename A>
  contig_range( std::vector<T, A>& arr ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
};

void mulArrayImpl( contig_range<int> arr, const int multiplier );

template<std::size_t N>
void mulArray( std::array<int, N>& arr, const int multiplier ) {
  mulArrayImpl( contig_range<int>(arr), multiplier );
}

(не проверял, но дизайн должен работать).

Затем в вашем .cppфайле:

void mulArrayImpl(contig_range<int> rng, const int multiplier) {
  for(auto& e : rng) {
    e *= multiplier;
  }
}

У этого есть обратная сторона: код, который перебирает содержимое массива, не знает (во время компиляции), насколько велик массив, что может привести к оптимизации затрат. Его преимущество в том, что реализация не обязательно должна быть в заголовке.

Будьте осторожны при явном построении a contig_range, так как если вы передадите его, setон будет предполагать, что setданные являются смежными, что является ложным, и повсюду будет вести себя неопределенно. Единственные два stdконтейнера, с которыми это гарантированно будет работать, - это vectorи array(и массивы в стиле C, как это бывает!). dequeнесмотря на то, что произвольный доступ не является непрерывным (что опасно, он непрерывен небольшими порциями!), listдаже не близок, а ассоциативные (упорядоченные и неупорядоченные) контейнеры одинаково не смежны.

Итак, три конструктора, которые я реализовал std::array, std::vectorи массивы в стиле C, которые в основном покрывают основы.

Реализация []легко , как хорошо, так и между for()и []что большая часть того, что вы хотите, arrayибо, не так ли?

Якк - Адам Неврамонт
источник
Разве это не просто перенос шаблона в другое место?
GManNickG
@GManNickG вроде. Заголовок получает очень короткую templateфункцию почти без деталей реализации. ImplФункция не является templateфункцией, и поэтому вы можете успешно скрыть реализацию в .cppфайл по вашему выбору. Это действительно грубый вид стирания типов, когда я извлекаю возможность перебирать смежные контейнеры в более простой класс, а затем передаю его через ... (хотя multArrayImplпринимает templateв качестве аргумента, это не templateсам по себе).
Yakk - Adam Nevraumont
Я понимаю, что этот класс представления массива / прокси-сервера массива иногда бывает полезен. Я предлагаю передать начало / конец контейнера в конструкторе, чтобы вам не приходилось писать конструктор для каждого контейнера. Также я бы не стал писать '& * std :: begin (arr)', поскольку разыменование и получение адреса здесь не нужны, поскольку std :: begin / std :: end уже возвращает итератор.
Ricky65
@ Ricky65 Если вы используете итераторы, вам нужно раскрыть реализацию. Если вы используете указатели, вы этого не сделаете. В &*разыменовывает итератор (который не может быть указателем), а затем делает указатель на адрес. Для непрерывных данных в памяти указатель на beginи указатель на один за другим endтакже являются итераторами с произвольным доступом, и они одного и того же типа для каждого непрерывного диапазона по типу T.
Якк - Адам Неврамонт