В чем смысл std:: bind и std:: thread всегда копировать аргументы?

Довольно хорошо известно, что поведение std:: bind и std:: thread по умолчанию заключается в том, что он будет копировать (или перемещать) переданные ему аргументы и использовать ссылочную семантику, нам придется использовать ссылочные оболочки.

  • Кто-нибудь знает, почему это делает хорошее поведение по умолчанию? Особенно в С++ 11 с ссылкой на rvalue и безупречной пересылкой мне кажется, что имеет смысл просто отлично форсировать аргументы.

  • std:: make_shared, хотя не всегда копирует/перемещает, а просто отлично передает предоставленные аргументы. Почему здесь есть два, казалось бы, различного поведения аргументов пересылки? (std:: thread и std:: bind, которые всегда копируют/перемещают vs std:: make_shared, которые этого не делают)

Ответ 1

make_shared пересылает конструктору, который вызывается сейчас. Если конструктор использует вызов по ссылочной семантике, он получит ссылку; если он вызывает по значению, он сделает копию. Здесь нет проблем.

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

Ответ 2

Для std::bind и std::thread вызов функции по заданным аргументам отложен с сайта вызова. В обоих случаях точно, когда функция будет вызвана, просто неизвестно.

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

К сожалению.

Lambdas может это сделать, потому что вам предоставляется возможность решать по каждому захвату, хотите ли вы захватить по ссылке или значению. Используя std::ref, вы можете привязать параметр по ссылке.

Ответ 3

Наиболее вероятной причиной является просто то, что С++ использует семантику значения по умолчанию почти везде. И использование ссылок может легко создавать проблемы, связанные с временем жизни упомянутого объекта.

Ответ 4

std:: bind создает вызываемый, который отсоединяется от сайта вызова std::bind, поэтому имеет смысл зафиксировать все аргументы по значению по умолчанию.

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

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

Ответ 5

Я действительно написал небольшую утилиту, которая создает замедленный функтор обращения (несколько std::bind -like, но без вложенных выражений привязки/замещающих функций). Моей главной мотивацией был этот случай, который я нашел контр-интуитивным:

using pointer_type = std::unique_ptr<int>;
pointer_type source();
void sink(pointer_type p);

pointer_type p = source();

// Either not valid now or later when calling bound()
// auto bound = std::bind(sink, std::move(p));
auto bound = std::bind(
    [](pointer_type& p) { sink(std::move(p)); }
    , std::move(p) );
bound();

Причиной для этого адаптера (который перемещает свой аргумент ref lvalue в sink) является то, что возвращаемая оболочка вызова std::bind всегда пересылает связанные аргументы как lvalues. Это не было проблемой, например, boost::bind в С++ 03, так как этот именующий будет либо привязка к опорному аргументу базового объекта или Callable к значению аргументу через копию. Здесь не работает, так как pointer_type является только перемещением.

Понимание, которое я получил, состоит в том, что есть две вещи, которые нужно учитывать: как должны храниться связанные аргументы и как их следует восстанавливать (т.е. передать объект Callable). Элемент управления std::bind предоставляет вам следующее: аргументы сохраняются в неглубоком (с использованием std::ref) или обычным образом (используя std::decay с совершенным вперед); они всегда восстанавливаются как lvalues ​​(с cv-квалификаторами, унаследованными от собственной оболочки вызова). За исключением того, что вы можете обойти последнее с помощью небольшого адаптивного лямбда-выражения адаптера на месте, как я это делал.

Это, возможно, большой контроль и много выражения для относительно небольшого изучения. В сравнении моя утилита имеет семантику, такую ​​как bind(f, p) (распад и сохранение копии, восстановление как lvalue), bind(f, ref(p)) (сохранить мелко, восстановить как lvalue), bind(f, std::move(p)) (разложить и сохранить из перемещения, восстановить как rvalue), bind(f, emplace(p)) (распад и сохранение из перемещения, восстановление как lvalue). Это похоже на изучение EDSL.