Есть ли причина, по которой стандартные алгоритмы принимают лямбда-выражения по значению?

Поэтому я задал вопрос здесь: Lambda работает в последней версии Visual Studio, но не работает в других местах, на что я получил ответ, что мой код был определен реализацией, так как стандарт 25.1 [gorithms.general] 10 говорит:

Если не указано иное, алгоритмы, которые принимают функциональные объекты в качестве аргументов, могут свободно копировать эти функциональные объекты. Программистам, для которых важна идентификация объекта, следует рассмотреть возможность использования класса-обертки, который указывает на некопированный объект реализации, такой как reference_wrapper<T>

Я просто хотел бы причину, почему это происходит? Нам всю жизнь говорят, что мы берем объекты по ссылке, почему тогда стандарт воспринимает функциональные объекты по значению, а в моем связанном вопросе создание копий этих объектов еще хуже? Есть ли какое-то преимущество, которое я не понимаю, чтобы делать это таким образом?

2 ответа

Решение

std предполагается, что функциональные объекты и итераторы могут свободно копировать.

std::ref предоставляет метод для превращения объекта функции в псевдо-ссылку с совместимым operator() который использует ссылку вместо семантики значения. Так что ничего ценного не потеряно.

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

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

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

Классическим примером является количество operator= что сломать, когда this и аргумент ссылается на тот же объект. Но любая функция, которая принимает две ссылки или указатели одного типа, имеет одну и ту же проблему.

Но даже одна ссылка может сломать ваш код. Давайте посмотрим на sort, В псевдокоде:

void sort( Iterator start, Iterator end, Ordering order )

Теперь давайте сделаем заказ по ссылке:

void sort( Iterator start, Iterator end, Ordering const& order )

Как насчет этого?

std::function< void(int, int) > alice;
std::function< void(int, int) > bob;
alice = [&]( int x, int y ) { std:swap(alice, bob); return x<y; };
bob = [&]( int x, int y ) { std:swap(alice, bob); return x>y; };

Теперь звоните sort( begin(vector), end(vector), alice ),

Каждый раз < называется, упомянутый order объект меняет смысл. Теперь это довольно смешно, но когда ты взял Ordering от const&оптимизатор должен был учитывать эту возможность и исключать ее при каждом вызове вашего кода заказа!

Вы не сделали бы вышеупомянутого (и фактически эта конкретная реализация - UB, поскольку это нарушило бы любые разумные требования на std::sort); но компилятор должен доказать, что вы не сделали что-то "подобное" (измените код в ordering) каждый раз, когда это следует order или вызывает это! Что означает постоянную перезагрузку состояния orderили обвинение в том, что вы совершили такое безумие.

Делать это, когда брать по стоимости, на порядок сложнее (и в основном требуется что-то вроде std::ref). У оптимизатора есть объект функции, он локальный, а его состояние локальное. Все, что хранится в нем, является локальным, и компилятор и оптимизатор знают, кто именно может изменить это на законных основаниях.

Каждая функция, которую вы пишете, принимает const& который когда-либо покидает свою "локальную область" (скажем, называемую библиотечной функцией C), не может принять состояние const& остался прежним после того, как вернулся. Он должен перезагрузить данные от того места, куда указывает указатель.

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

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

Я не уверен, что у меня есть для вас ответ, но если у меня есть правильное время жизни моего объекта, я думаю, что это переносимо, безопасно и добавляет ноль издержек или сложности:

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>


// @pre f must be an r-value reference - i.e. a temporary
template<class F>
auto resist_copies(F &&f) {
    return std::reference_wrapper<F>(f);
};

void removeIntervals(std::vector<double> &values, const std::vector<std::pair<int, int>> &intervals) {
    values.resize(distance(
            begin(values),
            std::remove_if(begin(values), end(values),
                           resist_copies([i = 0U, it = cbegin(intervals), end = cend(intervals)](const auto&) mutable 
    {
        return it != end && ++i > it->first && (i <= it->second || (++it, true));
    }))));
}


int main(int argc, char **args) {
    // Intervals of indices I have to remove from values
    std::vector<std::pair<int, int>> intervals = {{1,  3},
                                                  {7,  9},
                                                  {13, 13}};

    // Vector of arbitrary values.
    std::vector<double> values = {4.2, 6.4, 2.3, 3.4, 9.1, 2.3, 0.6, 1.2, 0.3, 0.4, 6.4, 3.6, 1.4, 2.5, 7.5};
    removeIntervals(values, intervals);
    // intervals should contain 4.2,9.1,2.3,0.6,6.4,3.6,1.4,7.5

    std:
    copy(values.begin(), values.end(), std::ostream_iterator<double>(std::cout, ", "));
    std::cout << '\n';
}
Другие вопросы по тегам