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

Аргументы 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? Или это подразумеваемая очевидная несогласованность?

Примечание. Объяснено, почему это не дублируется в комментарии ниже.

4b9b3361

Ответ 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 остался позади.

Это довольно сложно, однако, поэтому я бы рекомендовал просить сам комитет.