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

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

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

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


person Jonathan Mee    schedule 12.12.2016    source источник
comment
также см.: stackoverflow .com/questions/34825552/   -  person NathanOliver    schedule 12.12.2016
comment
Какова стоимость копирования лямбды, которая не содержит данных? После оптимизации вообще ничего. После оптимизации исключения копирования стоимость копирования даже сложного функтора, скорее всего, будет равна нулю.   -  person Richard Hodges    schedule 12.12.2016
comment
Нам всю жизнь говорят брать объекты по ссылке То есть большие объекты. Вы не увидите, чтобы я говорил людям передавать int и т. д. вместо const&.   -  person Baum mit Augen    schedule 12.12.2016
comment
@BaummitAugen Я имею в виду, что нам говорят не копировать strings по значению, и это похоже на указатель и size_t? Лямбда может быть намного больше, в зависимости от блока захвата.   -  person Jonathan Mee    schedule 12.12.2016
comment
@RichardHodges Если вы проверите вопрос, породивший этот, вы увидите, что в этом случае исключение копирования невозможно. Если вы считаете, что это будет полезно, я могу включить такой пример в вопрос.   -  person Jonathan Mee    schedule 12.12.2016
comment
Копирование строки @JonathanMee приводит к выделению новой памяти и копированию содержимого строкового буфера, если не действуют оптимизация малых строк или копирование при записи, хотя строки COW IIRC больше не разрешены...   -  person jaggedSpire    schedule 12.12.2016
comment
Есть много случаев, когда выгодно брать что-то по значению. Когда он маленький, когда вы знаете, что все равно собираетесь сделать копию и т. д. Язык стандарта позволяет реализации делать это, когда это лучше, чем брать ссылку.   -  person Edward Strange    schedule 12.12.2016
comment
@JonathanMee это похоже на указатель и size_t Однако это не то, что копируется. Копируется массив, на который указывает указатель, и он (потенциально) большой. Лямбда может быть намного больше Да, но обычно это не так. С другой стороны, более простое встраивание является реальным преимуществом.   -  person Baum mit Augen    schedule 12.12.2016
comment
@JonathanMee Я думаю, что одно из первоначальных намерений предикатов заключалось в том, что они представляют собой простые функциональные объекты без состояния. Если стандарт этого не говорит, то, вероятно, должен. ПО МОЕМУ МНЕНИЮ.   -  person Richard Hodges    schedule 12.12.2016
comment
@BaummitAugen Я думал о небольшой оптимизации string, но вы правы, это был плохой пример, нам сказали копировать по ссылке, чтобы предотвратить копирование строки в памяти.   -  person Jonathan Mee    schedule 12.12.2016
comment
@RichardHodges Но в этом и была вся мотивация лямбда-выражений, чтобы расширить функциональность стандартных алгоритмов? Разве не должны были быть алгоритмы, по крайней мере, вместе с ними?   -  person Jonathan Mee    schedule 12.12.2016
comment
@CrazyEddie Я думал, что когда объект больше размера указателя, больше нет смысла копировать по значению. Ваше утверждение подразумевает, что оно по-прежнему будет считаться небольшим и должно оцениваться по стоимости.   -  person Jonathan Mee    schedule 12.12.2016
comment
Я думал, что когда объект больше размера указателя, больше нет смысла копировать по значению. Я не думаю, что это правильно. Передача по ссылке требует больше затрат, чем просто копирование указателя за счет изменения семантики (а именно, путем введения потенциального псевдонима). Это, в свою очередь, влияет на оптимизацию.   -  person Baum mit Augen    schedule 12.12.2016
comment
@JonathanMee Предполагая, что скопированный аргумент действительно приводит к скопированию памяти (это не всегда так), все еще остается спор о том, что быстрее - одноразовая копия фрагмента памяти или постоянно повторяющаяся косвенная ссылка через ссылку на ( возможно постоянно меняющиеся) данные. Это немного похоже на дискуссию о том, что быстрее, линейный векторный поиск или карта. Вопреки интуиции, почти всегда линейный векторный поиск быстрее, потому что нет косвенного указателя, а память непрерывна.   -  person Richard Hodges    schedule 12.12.2016


Ответы (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& осталось прежним после его возвращения. Он должен перезагрузить данные оттуда, куда указывает указатель.

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

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

person Yakk - Adam Nevraumont    schedule 12.12.2016
comment
your type is very expensive to move or copy, for example, is a great reason где провести черту? как насчет большой структуры POD других POD, ни одна из которых не размещается в куче? - person johnbakers; 12.12.2016
comment
@johnbakers, когда пользователи ворчат, и профилирование вне всяких сомнений доказало, что это ваша копия, а не ваш ввод-вывод или ваш неэффективный алгоритм, или исчерпание кеша или ... (и т. Д.), Это узкое место. то есть почти никогда. - person Richard Hodges; 12.12.2016
comment
@johnbakers Вы должны крайне редко передавать большие структуры POD других структур POD. Это будет доля доли доли доли вашего кода; и если ваша функция зависит от состояния всей этой структуры POD, ваш код уже облажался. Если это не так, вы не должны передавать эту структуру POD. Глобальные данные под любым другим именем. Итак, теперь у нас есть доля ^ 4 вашей кодовой базы. С чем-то, что происходит редко (один раз на мегастроку кода), вы можете позволить себе профилировать, обсуждать и определять, стоит ли оно того. - person Yakk - Adam Nevraumont; 12.12.2016

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

#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';
}
person Richard Hodges    schedule 12.12.2016