Аргументы by-ref: это несогласованность между std:: thread и std:: bind?
std::bind
и std::thread
разделяют несколько принципов проектирования. Поскольку оба из них хранят локальные объекты, соответствующие переданным аргументам, нам нужно использовать либо std::ref
, либо std::cref
, если желательна эталонная семантика:
void f(int& i, double d) { /*...*/ }
void g() {
int x = 10;
std::bind(f, std::ref(x), _1) (3.14);
std::thread t1(f, std::ref(x), 3.14);
//...
}
Но я заинтригован недавним личным открытием: std::bind
позволит вам передать значение в приведенном выше случае, хотя это не то, что обычно требуется.
std::bind(f, x, _1) (3.14); // Usually wrong, but valid.
Однако это не относится к std::thread
. Следующее приведет к ошибке компиляции.
std::thread t2(f, x, 3.14); // Usually wrong and invalid: Error!
На первый взгляд я думал, что это ошибка компилятора, но ошибка действительно законна. Похоже, что шаблонная версия конструктора std::thread
не может правильно выводить аргументы из-за copy decaying require (tranforming int&
in int
), наложенным 30.3.1.2.
Возникает вопрос: почему бы не потребовать что-то похожее на аргументы std::bind
? Или это подразумеваемая очевидная несогласованность?
Примечание. Объяснено, почему это не дублируется в комментарии ниже.
Ответы
Ответ 1
Объект функции, возвращаемый bind
, предназначен для повторного использования (т.е. вызов вызывается несколько раз); поэтому он должен передавать свои связанные аргументы как lvalues, потому что вы не хотите переходить от указанных аргументов или позже, вызовы будут видеть перенесенный аргумент. (Аналогично, вы хотите, чтобы объект функции также назывался как lvalue.)
Эта проблема неприменима для std::thread
и друзей. Функция потока будет вызываться только один раз с предоставленными аргументами. Это совершенно безопасно, чтобы двигаться от них, потому что больше ничего не будет смотреть на них. Они фактически временные копии, сделанные только для нового потока. Таким образом, объект функции вызывается как rvalue, а аргументы передаются как rvalues.
Ответ 2
std::bind
был в основном устаревшим, когда он прибыл из-за существования лямбда. С улучшениями С++ 14 и С++ 17 std::apply
оставшиеся варианты использования для bind
в значительной степени исчезли.
Даже в С++ 11 случаи, когда bind
решили проблему, что лямбда не решает, где относительно редко.
С другой стороны, std::thread
решал несколько другую задачу. Для решения каждой проблемы не требуется гибкость bind
, вместо этого она может блокировать то, что обычно является плохим кодом.
В случае bind
Ссылка, переданная на f
, не будет x
, а скорее ссылкой на внутреннюю сохраненную копию x
. Это очень удивительно.
void f(int& x) {
++x;
std::cout << x << '\n';
};
int main() {
int x = 0;
auto b = std::bind(f, x);
b();
b();
b();
std::cout << x << '\n';
}
печатает
1
2
3
0
где последний 0
является исходным x
, а 1
2
и 3
- это увеличенная копия x
, хранящаяся в f
.
С lambda разница между изменяемым сохраненным состоянием и внешней ссылкой может быть прояснена.
auto b = [&x]{ f(x); };
против
auto b = [x]()mutable{ f(x); };
одна из копий x
затем повторно нажимает на нее f
, другая передает ссылку на x
на f
.
На самом деле нет способа сделать это с помощью bind
, не позволяя f
получить доступ к сохраненной копии x
в качестве ссылки.
Для std::thread
, если вы хотите изменить эту измененную локальную копию, вы просто используете lambda.
std::thread t1([x]()mutable{ f(x); });
На самом деле, я бы сказал, что большинство синтаксиса INVOKE в С++ 11, по-видимому, являются наследием отсутствия С++ 14 power lambdas и std::apply
на языке. Есть очень мало случаев, которые не решаются лямбдой и std::apply
(применять необходимо, поскольку лямбды не легко поддерживают переносные пакеты в них, а затем экстрагируют их внутри).
Но у нас нет машины времени, поэтому у нас есть несколько параллельных способов выразить идею вызова чего-либо в конкретном контексте на С++.
Ответ 3
Из того, что я могу сказать, thread
начинался с тех же правил, что и bind
, но был изменен в 2010 году N3090 принять указанное ограничение.
Используя это, чтобы разделить различные вклады, я считаю, что вы ищете LWG issue 929.
По иронии судьбы, похоже, намерение заключалось в том, чтобы сделать конструктор thread
менее ограниченным. Конечно, не упоминается bind
, хотя эта формулировка была позже применена и к async
(раздел "Очистка" после LWG 1315), поэтому я бы сказал, что bind
остался позади.
Это довольно сложно, однако, поэтому я бы рекомендовал просить сам комитет.