Уникати, якщо заява всередині циклу?


116

У мене є клас з ім'ям , Writerякий має функцію , writeVectorяк так:

void Drawer::writeVector(vector<T> vec, bool index=true)
{
    for (unsigned int i = 0; i < vec.size(); i++) {
        if (index) {
            cout << i << "\t";
        }
        cout << vec[i] << "\n";
    }
}

Я намагаюся не мати дублюючого коду, при цьому все ще переживаю за продуктивність. У функції я if (index)перевіряю кожен раунд моєї for-loop, хоча результат завжди однаковий. Це проти "турбуватися про виступ".

Я легко міг цього уникнути, поставивши чек поза моїм- forпетлею. Однак я отримаю набір дублюючого коду:

void Drawer::writeVector(...)
{
    if (index) {
        for (...) {
            cout << i << "\t" << vec[i] << "\n";
        }
    }
    else {
        for (...) {
            cout << vec[i] << "\n";
        }
    }
}

Тож це обидва «погані» рішення для мене. Те, що я думав, це дві приватні функції, одна з них виводить з індексу, а потім викликає іншу. Інший лише випереджає значення. Однак я не можу розібратися, як ним користуватися зі своєю програмою, мені все одно потрібна ifперевірка, щоб побачити, до кого зателефонувати ...

Відповідно до проблеми, поліморфізм здається правильним рішенням. Але я не бачу, як мені тут користуватися. Який би кращий спосіб вирішити подібну проблему?

Це не реальна програма, мені просто цікаво дізнатися, як слід вирішувати подібну проблему.


8
@JonathonReinhart Можливо, деякі люди хочуть навчитися програмуванню і цікавляться, як вирішити проблеми?
Skamah One

9
Я дав це питання +1. Цей вид оптимізації може не часто бути необхідним, але, по-перше, вказівка ​​на цей факт може бути частиною відповіді, по-друге, рідкісні типи оптимізації все ще є дуже актуальними для програмування.
jogojapan

31
Питання полягає у гарному дизайні, який дозволяє уникнути дублювання коду та складної логіки всередині циклу. Це гарне запитання, не потрібно його спростовувати.
Алі

5
Цікаве питання, як правило, передачі циклу перетворення в компіляторі вирішать це дуже ефективно. якщо функція достатньо мала, як ця, інлайнер подбає про неї і, швидше за все, повністю знищить гілку. Я вважаю за краще змінити код, поки інлайнер не буде вкладений в код, а не вирішити це за допомогою шаблонів.
Олексій

5
@JonathonReinhart: Так? Перша редакція питання практично ідентична цій. Ваше "навіщо вам байдуже?" коментар на 100% не стосується всіх змін. Що стосується догани тобі публічно - це не лише ти, а багато людей тут викликають цю проблему. Коли заголовок "уникнення, якщо висловлювання всередині циклу" , має бути досить очевидним, що питання є загальним, а приклад - лише для ілюстрації . Ти нікому не допомагаєш, коли ігноруєш питання і змушуєш ОП виглядати дурним через конкретний наочний приклад, який він використав.
користувач541686

Відповіді:


79

Перейти в тіло петлі як функтор. Це стає накресленим під час компіляції, без виконання пенальті.

Ідея передавати те, що змінюється, всюди поширена в стандартній бібліотеці C ++. Її називають стратегічною схемою.

Якщо вам дозволяється використовувати C ++ 11, ви можете зробити щось подібне:

#include <iostream>
#include <set>
#include <vector>

template <typename Container, typename Functor, typename Index = std::size_t>
void for_each_indexed(const Container& c, Functor f, Index index = 0) {

    for (const auto& e : c)
        f(index++, e);
}

int main() {

    using namespace std;

    set<char> s{'b', 'a', 'c'};

    // indices starting at 1 instead of 0
    for_each_indexed(s, [](size_t i, char e) { cout<<i<<'\t'<<e<<'\n'; }, 1u);

    cout << "-----" << endl;

    vector<int> v{77, 88, 99};

    // without index
    for_each_indexed(v, [](size_t , int e) { cout<<e<<'\n'; });
}

Цей код не є ідеальним, але ви розумієте.

У старому C ++ 98 це виглядає приблизно так:

#include <iostream>
#include <vector>
using namespace std;

struct with_index {
  void operator()(ostream& out, vector<int>::size_type i, int e) {
    out << i << '\t' << e << '\n';
  }
};

struct without_index {
  void operator()(ostream& out, vector<int>::size_type i, int e) {
    out << e << '\n';
  }
};


template <typename Func>
void writeVector(const vector<int>& v, Func f) {
  for (vector<int>::size_type i=0; i<v.size(); ++i) {
    f(cout, i, v[i]);
  }
}

int main() {

  vector<int> v;
  v.push_back(77);
  v.push_back(88);
  v.push_back(99);

  writeVector(v, with_index());

  cout << "-----" << endl;

  writeVector(v, without_index());

  return 0;
}

Знову ж таки, код далеко не ідеальний, але він дає вам ідею.


4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Це таке рішення, яке я шукав, воно працює як шарм :) Ви можете покращити його за допомогою декількох коментарів (спочатку були проблеми з його розумінням), але я отримав це так, що немає проблем :)
Skamah One

1
Я радий, що це допомогло! Перевірте моє оновлення з кодом C ++ 11, він менш роздутий порівняно з версією C ++ 98.
Алі

3
Нітпік: це добре в прикладі ОП, оскільки тіло циклу настільки мало, але якби він був більшим (уявіть десяток рядків коду, а не один-єдиний cout << e << "\n";), все одно було б певне дублювання коду.
syam

3
Чому в прикладі C ++ 03 використовуються структури та перевантаження оператора? Чому б просто не зробити дві функції і передати їм покажчики?
Малькольм

2
@Malcolm Inlining. Якщо вони є структурою, швидше за все, виклики функцій можуть бути окреслені. Якщо ви передаєте функцію вказівника, ці шанси, що ці дзвінки не можуть бути вписані.
Алі

40

У функції я роблю перевірку if (index) на кожному раунді моєї for-loop, хоча результат завжди однаковий. Це проти "турбуватися про виступ".

Якщо це дійсно так, передбачувач галузі не матиме жодних проблем передбачити (постійний) результат. Таким чином, це призведе лише до легких витрат на помилки в перших кількох ітераціях. З точки зору продуктивності не варто хвилюватися

У цьому випадку я виступаю за збереження тесту всередині циклу для наочності.


3
Це просто приклад, я тут, щоб дізнатися, як слід вирішувати подібну проблему. Мені просто цікаво, навіть не створюючи реальної програми. Чи слід було це згадати у запитанні.
Skamah One

40
У цьому випадку пам’ятайте, що передчасна оптимізація є коренем усього зла . Під час програмування завжди зосередьтеся на читанні коду та переконайтеся, що інші розуміють, що ви намагаєтесь зробити. Розгляньте мікро-оптимізацію та різні хаки лише після профілювання вашої програми та визначення точок доступу . Ніколи не слід розглядати оптимізацію, не встановлюючи потреби в них. Дуже часто проблеми з роботою не є там, де ви їх очікуєте.
Marc Claesen

3
І в цьому конкретному прикладі (добре, зрозуміло, це лише приклад), дуже ймовірно, що час, витрачений на контроль циклу, і якщо тест майже невидимий, крім часу, витраченого на IO. Це часто проблема C ++: вибір між читабельністю ціною технічного обслуговування та (гіпотетичною) ефективністю.
kriss

8
Ви припускаєте, що код працює на процесорі, який має починати передбачення гілок. Більшість систем, на яких працює C ++, цього не роблять. (Хоча, напевно, більшість систем з корисною std::coutсправою)
Бен Войгт

2
-1. Так, прогнозування галузей тут буде добре працювати. Так, умова фактично може бути піднята компілятором поза циклу. Так, POITROAE. Але гілки в циклі - це небезпечна річ, яка часто впливає на ефективність роботи, і я не думаю, що їх відхилення просто кажучи "передбачення гілки" - це хороша порада, якщо хтось дійсно піклується про ефективність. Найбільш помітним прикладом є те, що векторизуючий компілятор потребує передбачення для вирішення цього питання, створюючи менш ефективний код, ніж для циклів без гілок.
Дуб

35

Щоб розширити відповідь Алі, що є абсолютно правильним, але все ж дублює деякий код (частина тіла циклу, цього, на жаль, навряд чи можна уникнути при використанні шаблону стратегії) ...

У цьому конкретному випадку дублювання коду не так багато, але є спосіб зменшити його ще більше, що стане в нагоді, якщо тіло функції більше, ніж кілька інструкцій .

Ключовим моментом є використання здатності компілятора виконувати постійне усунення складних / мертвих кодів . Це можна зробити, вручну зіставивши значення часу виконання на значення часу indexкомпіляції (це легко зробити, коли є лише обмежена кількість випадків - у цьому випадку два) та використовувати аргумент нетипового шаблону, який відомий при компіляції -час:

template<bool index = true>
//                  ^^^^^^ note: the default value is now part of the template version
//                         see below to understand why
void writeVector(const vector<int>& vec) {
    for (size_t i = 0; i < vec.size(); ++i) {
        if (index) { // compile-time constant: this test will always be eliminated
            cout << i << "\t"; // this will only be kept if "index" is true
        }
        cout << vec[i] << "\n";
    }
}

void writeVector(const vector<int>& vec, bool index)
//                                            ^^^^^ note: no more default value, otherwise
//                                            it would clash with the template overload
{
    if (index) // runtime decision
        writeVector<true>(vec);
        //          ^^^^ map it to a compile-time constant
    else
        writeVector<false>(vec);
}

Таким чином ми закінчуємо складеним кодом, який еквівалентний вашому другому прикладу коду (зовнішній if/ внутрішній for), але без дублювання коду самостійно. Тепер ми можемо зробити версію шаблону writeVectorтакою ж складною, як ми хочемо, завжди буде єдиний фрагмент коду для підтримки.

Зверніть увагу, як версія шаблону (яка приймає константу часу компіляції у вигляді аргументу шаблону нетипового типу) та нешаблонна версія (яка приймає змінну часу виконання як аргумент функції) перевантажена. Це дозволяє вибрати найбільш релевантну версію залежно від ваших потреб, маючи досить схожий, легкий для запам'ятовування синтаксис в обох випадках:

writeVector<true>(vec);   // you already know at compile-time which version you want
                          // no need to go through the non-template runtime dispatching

writeVector(vec, index);  // you don't know at compile-time what "index" will be
                          // so you have to use the non-template runtime dispatching

writeVector(vec);         // you can even use your previous syntax using a default argument
                          // it will call the template overload directly

2
Зауважте, що ви видалили дублювання коду за рахунок ускладнення логіки всередині циклу. Я не бачу це ні краще, ні гірше, ніж те, що я запропонував для цього конкретного простого прикладу. +1 у будь-якому разі!
Алі

1
Мені подобається ваша пропозиція, оскільки вона показує іншу можливу оптимізацію. Цілком можливо, що індекс міг бути константою шаблону з самого початку. У цьому випадку його можна замінити постійною тривалістю виконання абонентом writeVector, а writeVector змінити на якийсь шаблон. Уникаючи подальших змін оригінального коду.
kriss

1
@kriss: Насправді моє попереднє рішення вже дозволило, що якщо ви телефонували doWriteVectorбезпосередньо, але я згоден, це ім'я було невдалим. Я просто змінив це, щоб було дві перевантажені writeVectorфункції (один шаблон, інший регулярна функція), щоб результат був більш однорідним. Дякую за пропозицію. ;)
сім

4
ІМО це найкраща відповідь. +1
користувач541686

1
@Mehrdad За винятком того, що він не відповідає на початкове запитання Уникнення, якщо заява всередині циклу for? Однак він відповідає, як уникнути штрафу за продуктивність. Що стосується "дублювання", потрібен би більш реалістичний приклад із випадками використання, щоб побачити, як найкраще розглянути фактичні результати. Як я вже говорив, я підтримав цю відповідь.
Алі

0

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

#include <cstdio>
#include <iterator>

void write_vector(int* begin, int* end, bool print_index = false) {
    unsigned index = 0;
    for(int* it = begin; it != end; ++it) {
        if (print_index) {
            std::printf("%d: %d\n", index, *it);
        } else {
            std::printf("%d\n", *it);
        }
        ++index;
    }
}

int my_vector[] = {
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
};


int main(int argc, char** argv) {
    write_vector(std::begin(my_vector), std::end(my_vector));
}

Я використовую такий командний рядок для його складання:

g++ --version
g++ (GCC) 4.9.1
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
g++ -O3 -std=c++11 main.cpp

Потім давайте скидаємо збірку:

objdump -d a.out | c++filt > main.s

Результат складання write_vector:

00000000004005c0 <write_vector(int*, int*, bool)>:
  4005c0:   48 39 f7                cmp    %rsi,%rdi
  4005c3:   41 54                   push   %r12
  4005c5:   49 89 f4                mov    %rsi,%r12
  4005c8:   55                      push   %rbp
  4005c9:   53                      push   %rbx
  4005ca:   48 89 fb                mov    %rdi,%rbx
  4005cd:   74 25                   je     4005f4 <write_vector(int*, int*, bool)+0x34>
  4005cf:   84 d2                   test   %dl,%dl
  4005d1:   74 2d                   je     400600 <write_vector(int*, int*, bool)+0x40>
  4005d3:   31 ed                   xor    %ebp,%ebp
  4005d5:   0f 1f 00                nopl   (%rax)
  4005d8:   8b 13                   mov    (%rbx),%edx
  4005da:   89 ee                   mov    %ebp,%esi
  4005dc:   31 c0                   xor    %eax,%eax
  4005de:   bf a4 06 40 00          mov    $0x4006a4,%edi
  4005e3:   48 83 c3 04             add    $0x4,%rbx
  4005e7:   83 c5 01                add    $0x1,%ebp
  4005ea:   e8 81 fe ff ff          callq  400470 <printf@plt>
  4005ef:   49 39 dc                cmp    %rbx,%r12
  4005f2:   75 e4                   jne    4005d8 <write_vector(int*, int*, bool)+0x18>
  4005f4:   5b                      pop    %rbx
  4005f5:   5d                      pop    %rbp
  4005f6:   41 5c                   pop    %r12
  4005f8:   c3                      retq   
  4005f9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
  400600:   8b 33                   mov    (%rbx),%esi
  400602:   31 c0                   xor    %eax,%eax
  400604:   bf a8 06 40 00          mov    $0x4006a8,%edi
  400609:   48 83 c3 04             add    $0x4,%rbx
  40060d:   e8 5e fe ff ff          callq  400470 <printf@plt>
  400612:   49 39 dc                cmp    %rbx,%r12
  400615:   75 e9                   jne    400600 <write_vector(int*, int*, bool)+0x40>
  400617:   eb db                   jmp    4005f4 <write_vector(int*, int*, bool)+0x34>
  400619:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

Ми бачимо, що при випробовуванні функції ми перевіряємо значення і переходимо до однієї з двох можливих циклів:

  4005cf:   84 d2                   test   %dl,%dl
  4005d1:   74 2d                   je     400600 <write_vector(int*, int*, bool)+0x40>

Звичайно, це працює лише в тому випадку, якщо компілятор здатний виявити, що умова є інваріантною. Зазвичай він прекрасно працює для прапорів і простих вбудованих функцій. Але якщо умова "складна", подумайте про використання підходів з інших відповідей.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.