Подтвердить что ты не робот

Для цикла vs std:: for_each с лямбдой

Рассмотрим функцию шаблона, написанную на С++ 11, которая выполняет итерацию над контейнером. Пожалуйста, исключите из рассмотрения синтаксис цикла диапазона, потому что он еще не поддерживается компилятором, с которым я работаю.

template <typename Container>
void DoSomething(const Container& i_container)
  {
  // Option #1
  for (auto it = std::begin(i_container); it != std::end(i_container); ++it)
    {
    // do something with *it
    }

  // Option #2
  std::for_each(std::begin(i_container), std::end(i_container), 
    [] (typename Container::const_reference element)
    {
    // do something with element
    });
  }

Что такое за/против цикла for vs std::for_each в терминах:

a) производительность? (Я не ожидаю никакой разницы)

b) читаемость и ремонтопригодность?

Здесь я вижу много недостатков for_each. Он не принимал бы массив c-style, в то время как цикл был бы. Объявление формального параметра лямбда настолько подробное, что невозможно использовать auto. Невозможно выйти из for_each.

В пред-С++ 11 дней аргументам против for требовалась спецификация типа для итератора (больше не держится) и простая возможность обмануть условие цикла (я никогда не делал таких ошибка через 10 лет).

Как вывод, мои мысли о for_each противоречат общему мнению. Что мне здесь не хватает?

4b9b3361

Ответ 1

Я думаю, что есть некоторые другие различия, пока не затронутые ответами.

  • a for_each может принимать любой подходящий вызываемый объект, позволяя "перерабатывать" тело цикла для разных циклов. Например (псевдокод)

    for( range_1 ) { lengthy_loop_body }    // many lines of code
    for( range_2 ) { lengthy_loop_body }    // the same many lines of code again
    

    становится

    auto loop_body = some_lambda;           // many lines of code here only
    std::for_each( range_1 , loop_body );   // a single line of code
    std::for_each( range_2 , loop_body );   // another single line of code
    

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

  • другое отличие касается выхода из цикла (с break или return в цикле for). Насколько мне известно, в цикле for_each это может быть сделано только путем исключения исключения. Например

    for( range )
    {
      some code;
      if(condition_1) return x; // or break
      more code;
      if(condition_2) continue;
      yet more code;
    }
    

    становится

    try {
      std::for_each( range , [] (const_reference x)
                    {
                      some code;
                      if(condition_1) throw x;
                      more code;
                      if(condition_2) return;
                      yet more code;
                    } );
    } catch(const_reference r) { return r; }
    

    с теми же эффектами относительно вызова деструкторов для объектов с областью действия тела цикла и тела функции (вокруг цикла).

  • Основное преимущество for_each - IMHO, что можно перегрузить его для определенных типов контейнеров, когда простая итерация не так эффективна. Например, рассмотрим контейнер, который содержит связанный список блоков данных, каждый блок, содержащий смежный массив элементов, аналогичный (опускающий нерелевантный код)

    namespace my {
      template<typename data_type, unsigned block_size>
      struct Container
      {
        struct block
        {
          const block*NEXT;
          data_type DATA[block_size];
          block() : NEXT(0) {}
        } *HEAD;
      };
    }
    

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

    namespace my {
      template<typename data_type, unsigned block_size>
      struct Container
      {
        struct iterator
        {
          const block*B;
          unsigned I;
          iterator() = default;
          iterator&operator=(iterator const&) = default;
          iterator(const block*b, unsigned i) : B(b), I(i) {}
          iterator& operator++()
          {
            if(++I==block_size) { B=B->NEXT; I=0; }    // one comparison and branch
            return*this;
          }
          bool operator==(const iterator&i) const
          { return B==i.B && I==i.I; }                 // one or two comparisons
          bool operator!=(const iterator&i) const
          { return B!=i.B || I!=i.I; }                 // one or two comparisons
          const data_type& operator*() const
          { return B->DATA[I]; }
        };
        iterator begin() const
        { return iterator(HEAD,0); }
        iterator end() const
        { return iterator(0,0); }
      };
    }
    

    этот тип итератора корректно работает с for и for_each, например

    my::Container<int,5> C;
    for(auto i=C.begin();
        i!=C.end();              // one or two comparisons here
        ++i)                     // one comparison here and a branch
      f(*i);
    

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

    namespace my {
      template<typename data_type, int block_size, typename FuncOfDataType>
      FuncOfDataType&&
      for_each(typename my::Container<data_type,block_size>::iterator i,
               typename my::Container<data_type,block_size>::iterator const&e,
               FuncOfDataType f)
      {
        for(; i.B != e.B; i.B++,i.I=0)
          for(; i.I != block_size; i.I++)
            f(*i);
        for(; i.I != e.I; i.I++)
          f(*i);
        return std::move(f);
      }
    }
    using my::for_each;     // ensures that the appropriate
    using std::for_each;    // version of for_each() is used
    

    который требует только одного сравнения для большинства итераций и не имеет ответвлений (обратите внимание, что ветки могут иметь неприятное влияние на производительность). Обратите внимание, что нам не нужно определять это в пространстве имен std (что может быть незаконным), но может гарантировать, что правильная версия используется соответствующими директивами using. Это эквивалентно using std::swap; при специализировании swap() для определенных пользовательских типов.

Ответ 2

Что касается производительности, цикл for вызывает std::end несколько раз, а std::for_each - нет. Это может привести или не привести к разнице в производительности в зависимости от используемого контейнера.

Ответ 3

  • Версия std::for_each будет посещать каждый элемент ровно один раз. Кто-то, читающий код, может знать, что, как только они видят std::for_each, так как ничего не может быть сделано в лямбде, чтобы запутаться с итератором. В традиционном для цикла вам необходимо изучить тело цикла для необычного потока управления (continue, break, return) и потушить с помощью итератора (например, в этом случае пропустить следующий элемент с помощью ++it).

  • Вы можете тривиально изменить алгоритм в лямбда-решении. Например, вы можете сделать алгоритм, который посещает каждый n-й элемент. Во многих случаях вам все равно не нужен цикл for, но другой алгоритм, например copy_if. Использование алгоритма + лямбда, часто более поддается изменению и немного более кратким.

  • С другой стороны, программисты гораздо более привыкли к традиционным для циклов, поэтому они могут найти сложный алгоритм + лямбда.

Ответ 4

Во-первых, я не вижу большой разницы между этими двумя, потому что for_each реализуется с использованием цикла. Но обратите внимание, что for_each - это функция, которая имеет возвращаемое значение.

Во-вторых, я буду использовать синтаксис цикла диапазона, который будет доступен в этом случае, так как этот день скоро придет.

Ответ 5

Действительно, в случае использования выражения Lambda вам необходимо объявить тип и имя параметра, поэтому ничего не выиграно.

Но это будет потрясающе, как только вы захотите вызвать одну (именованную) функцию или функцию-объект с этим. (Помните, что вы можете комбинировать функциональные вещи через std::bind.)

Книги Скотта Мейерса (я считаю, что Эффективный STL) описывают такие стили программирования очень хорошо и ясно.