Как избежать дублирования кода при переборе вектора‹int› или числового диапазона?

Мне нужно выполнить довольно сложный фрагмент кода в цикле, но цикл либо над вектором, либо над числовым диапазоном целых чисел. Решение о типе цикла принимается во время выполнения:

if(!int_vector_provided){
   for(int i=0;i<N;++i){ // iterate over a numeric range
      // complex code depending on i
   }
} else {
   for(int i: int_vector){ // iterate over a vector
      // the same complex code
   }
}

Проблема в том, что «сложный код» трудно преобразовать в функцию, поскольку он зависит от множества локальных и глобальных переменных. Также нежелательно делать этот код захватывающей лямбдой, так как эта часть критична для производительности. Если используется числовой диапазон, то он обычно очень велик (миллионы), поэтому очень неэффективно создавать вектор последовательных чисел такого размера.

На самом деле мне нужна пара итераторов, которые могут быть назначены либо началу/концу вектора, либо началу/концу числового диапазона. Что-то типа:

SomeCleverIterator b,e;

if(int_vector_provided){
   b = int_vector.begin();
   e = int_vector.end();  
} else {
   // Iterators to numeric range
   // may be boost::counting_range(0,N) ???
   // but how to make boost::range iterators and 
   // to vector<int>::iterator convertible to the same type??
   b = ???;
   e = ???;
}

for(SomeCleverIterator it=b;it!=e;it++){
      // complex code
}

Я пытался поиграть с boost::counting_range, но его итераторы не конвертируются в vector<int>::iterator, так что это не помогает.

Единственный способ, который я вижу, это создать собственный шаблон класса с итераторами и явно создать его экземпляр для вектора и пары целых чисел, но это выглядит излишним для такой «тривиальной» проблемы.

Есть ли лучший способ?


person yesint    schedule 02.02.2020    source источник
comment
Почему бы просто не перебрать вектор как числовой диапазон [0, size())? Также будьте осторожны, если вы изменяете код, критически важный для производительности, подобным этому. перемещение if/else в цикл, вероятно, взаимодействует с прогнозированием ветвлений и может отрицательно сказаться на производительности.   -  person midor    schedule 02.02.2020
comment
@midor, потому что вектор может содержать любые непоследовательные целые числа, такие как {1 100 150 1000}   -  person yesint    schedule 02.02.2020
comment
XY проблема. Вместо int_vector_provided должно быть 2 отдельные функции, а сложная часть кода должна быть извлечена во вспомогательную встроенную функцию. Или предоставьте минимально воспроизводимый пример, подробно объясняющий вашу ситуацию.   -  person rustyx    schedule 02.02.2020
comment
Это читается как классическая проблема XY. Если у вас есть два цикла в одной и той же функции с одним и тем же телом, то рефакторинг для размещения сложного кода в функции тривиален. Если сложный код зависит от переменных В вызывающем объекте, передайте их в качестве аргументов (или, в худшем случае, создайте структуру, содержащую набор ссылок на них, и передайте ее в качестве аргумента). Если сложный код зависит от статики, объявленной перед вашим вызывающим кодом, то размещение новой функции непосредственно выше будет иметь видимость той же статики.   -  person Peter    schedule 02.02.2020
comment
Вы уже поняли, в чем собственно проблема. Глобальные переменные всегда будут вас кусать в долгосрочной перспективе. Лучше не затягивайте с этим дальше и исправьте это.   -  person 463035818_is_not_a_number    schedule 02.02.2020


Ответы (3)


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

И поскольку он будет использоваться только в этой одной функции, наиболее целесообразна общая лямбда:

auto f = [&](auto&& range) {
    for (int i : range) {
        // complex code depending on i
    }
};
if (int_vector_provided)
    f(int_vector);
else
    f(std::ranges::iota_view(0, N));

Я также использовал C++20 std::ranges::iota_view.

Конечно, это также может быть случаем зависти к флагу, и в этом случае вам следует просто разделить интерфейс на две разные функции, возможно, подкрепленные общей реализацией.

Кроме того, глобальное состояние должно быть сведено к минимуму, особенно изменяемое.

person Deduplicator    schedule 02.02.2020

Проблема в том, что «сложный код» трудно преобразовать в функцию, поскольку он зависит от множества локальных и глобальных переменных.

Это определенно звучит как причина, по которой вам следует провести рефакторинг этого кода.

Также нежелательно делать этот код захватывающей лямбдой, так как эта часть критична для производительности.

Измерили ли вы, что захват лямбды менее эффективен? Я бы подумал, что это должно быть примерно так же быстро, как код, и это определенно будет самое простое решение.

При этом я думаю, что ключом к вашей проблеме является полиморфизм. Поскольку вам нужно обрабатывать только два разных типа, я бы рекомендовал посмотреть std::variant. Вы бы использовали std:: variant<typename std::vector<int>::iterator, int> и выполняли бы все операции посетителями.

Изменить. Простое решение:

int index = 0;
const int end = int_vector_provided ? int_vector.size() : N;
for(; index < end; ++index) {
    int to_use = int_vector_provided ? vector[index] : index;
    // Do calculations with to_use
}

Это добавляет посторонние логические проверки для int_vector_provided, но я думаю, что оптимизатор будет работать довольно хорошо, если это const (я думаю, что const имеет здесь существенное значение в отношении производительности).

person n314159    schedule 02.02.2020
comment
В этом случае захват лямбды снижает производительность. Не очень критично, но измеримо. Я хотел бы избежать этого. Еще один момент против использования варианта и посетителей заключается в том, что сам код сложен (это действительно нетривиальный числовой код), поэтому я хотел бы минимизировать шаблон, если это возможно. - person yesint; 02.02.2020
comment
Шаблон с variant не будет очень большим. У вас есть 3 посетителя, один для сравнения, один для приращения (оба тривиальны) и один для разыменования итератора. После этого у вас есть int в обоих случаях и потом точно такой же код. - person n314159; 02.02.2020
comment
@yesint Я добавил очень низкотехнологичное решение. - person n314159; 02.02.2020

Напоминает мне более общий вопрос, который я однажды задал об итерации/генераторах. До C++20 не было удовлетворительного общего удовлетворительного решения. В настоящее время вы, вероятно, можете сделать это с помощью сопрограмм, которые возвращают следующий элемент и стирают его тип, но я еще не закодировал это.

В любом случае, в вашем случае проблема в коде сводится к двум основным моментам:

  1. у вас так много контекста, что вы не можете выделить функцию
  2. у вас не может быть переменной, которая может содержать оба итератора, потому что итератор, формирующий вектор, является итератором

Вы можете перебирать оба варианта, используя итерацию на основе индекса, но вам придется решать на каждой итерации, используете ли вы i или v[i]. Поднять условие, вероятно, еще быстрее.

Существует также сложное решение, заключающееся в реализации итератора-обертки, которое стирает тип итератора, но если у вас уже нет чего-то подобного, я бы посоветовал не использовать это решение, потому что оно будет сложным. , трудно понять правильно и, вероятно, непропорционально.

Вы не опубликовали контекст, поэтому я не могу сказать, насколько это возможно, но я чувствую, что лучший совет, который я могу вам дать, - это реорганизовать остальную часть кода, прежде чем вы коснетесь этой проблемы. Вполне вероятно, что сложность контекста делает это дублирование таким болезненным, а не само дублирование. Подъем такой проверки — обычная оптимизация, которая не всегда вредит удобству сопровождения/читабельности.

person midor    schedule 02.02.2020
comment
Да, генераторы, как в Python, решили бы такие проблемы раз и навсегда. Однако прагматичное решение в моем случае, вероятно, состоит в том, чтобы реорганизовать беспорядок в цикле и сделать его функцией или вызываемым классом. - person yesint; 02.02.2020