Страницы

Поиск по вопросам

воскресенье, 1 декабря 2019 г.

Различия auto и auto&& внутри диапазонного for цикла

#cpp #cpp11


Какая разница между 1 и 2? Когда что использовать?

for (auto   i : container){} // 1
for (auto&& i : container){} // 2

    


Ответы

Ответ 1



Когда у вас имеется объявление вида auto &&x = initializer; то данная rvalue ссылка &&x является особенной и носит название forwarding reference. Если инициализатор представляет собой lvalue, то эта ссылка принимает тип lvalue ссылки. Из стандарта C++ (Document Number: N4296, 7.1.6.4 auto specifier) ...Deduce a value for U using the rules of template argument deduction from a function call (14.8.2.1), where P is a function template parameter type and the corresponding argument is the initializer, or L in the case of direct-list-initialization. И далее (14.8.2.1 Deducing template arguments from a function cal) ...A forwarding reference is an rvalue reference to a cv-unqualified template parameter. If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction Что это означает? Это означает, что следующие два объявления будут эквивалентными int x = 0; auto &&r = x; и int x = 0; auto &r = x; Ниже приведена демонстрационная программа #include int main() { { int x = 0; std::cout << "x = " << x << std::endl; auto &&r = x; r = 10; std::cout << "x = " << x << std::endl; } { int x = 0; std::cout << "x = " << x << std::endl; auto &r = x; r = 10; std::cout << "x = " << x << std::endl; } } Ее вывод на консоль x = 0 x = 10 x = 0 x = 10 Из этого также следует, что когда вы имеете дело с for предложением на основе диапазона, и соответствующий итератор, возвращаемый функцией begin, или соответствующий указатель, после применения оператора разыменования возвращают lvalue значение исходного объекта (указатели всегда возвращают lvalue значение после разыменования), то вы можете изменить этот исходный объект используя объявление forwarding reference. Например, #include #include int main() { { int a[] = { 0 }; std::cout << "a[0] = " << a[0] << std::endl; auto &&r = *a; r = 10; std::cout << "a[0] = " << a[0] << std::endl; } { std::vector v(1); std::cout << "v[0] = " << v[0] << std::endl; auto &&r = *v.begin(); r = 10; std::cout << "v[0] = " << v[0] << std::endl; } } Вывод программы на консоль: a[0] = 0 a[0] = 10 v[0] = 0 v[0] = 10 Поэтому, например, для стандартных контейнеров, которые возвращают из разыменованного итератора ссылку на исходный объект, два подобных объявления for эквиваленны std::vector v { 1, 2, 3, 4, 5 }; for ( auto &&x : v ) x *= 2; и std::vector v { 1, 2, 3, 4, 5 }; for ( auto &x : v ) x *= 2; Так как в обоих случаях имеется выведенный тип переменной x как int &. Когда же используется объявление вида auto x = initializer; то тип переменной x выводится из спецификаторов типа инициализатора, игнорируя ссылки. То есть если даже инициализатор - это ссылочный тип, как, например, int &, тип переменной x будет int, а, следовательно вы не сможете изменить исходный объект, используя эту переменную. Например, #include #include int main() { { std::vector v{ 1, 2, 3, 4, 5 }; for (int x : v) std::cout << x << ' '; std::cout << std::endl; for (auto &&x : v) x *= 2; for (int x : v) std::cout << x << ' '; std::cout << std::endl; } std::cout << std::endl; { std::vector v{ 1, 2, 3, 4, 5 }; for (int x : v) std::cout << x << ' '; std::cout << std::endl; for (auto x : v) x *= 2; for (int x : v) std::cout << x << ' '; std::cout << std::endl; } } Сравните вывод двух кодовых блоков этой программы 1 2 3 4 5 2 4 6 8 10 1 2 3 4 5 1 2 3 4 5 Если же forwarding reference инициализируется rvalue значением, а не lvalue значением, то rvalue ссылка не будет превращаться в lvalue ссылку. Ниже показано это различие. #include int f() { static int x; return x; } int & g() { static int x; return x; } int main() { { auto &&x = f(); std::cout << "x = " << x << std::endl; x = 10; std::cout << "x = " << x << std::endl; std::cout << "f() = " << f() << std::endl; } std::cout << std::endl; { auto &&x = g(); std::cout << "x = " << x << std::endl; x = 10; std::cout << "x = " << x << std::endl; std::cout << "g() = " << g() << std::endl; } } Вывод программы на консоль x = 0 x = 10 f() = 0 x = 0 x = 10 g() = 10 Что касается вопроса, Когда что использовать? то лучше использовать auto & если вы хотите изменять значения в цикле, на которые будет ссылаться ссылка, либо const auto &, когда вы используете значения только для чтения. auto имеет смысл использовать для фундаментальных типов, например, арифметических типов или указателей), когда копирование объекта в создаваемую локальную переменную не является ресурсо-затратной операцией и не требуется изменить исходные объекты.

Ответ 2



Выбирать нужно в зависимости от контекста, о котором Вы совершенно не обмолвились. Например, следующий код: #include int main() { std::vector v; for (auto i: v) { } for (auto& i: v) { } // ошибка for (auto&& i: v) { } } даст ошибку для auto&, т.к. vector использует вспомогательный ссылочный класс, rvalue-объект которого не может быть связан с неконстанной ссылкой. Из-за прокси-класса возникает ещё одна интересная ситуация, что модификация i в циклах auto и auto&& будут вести себя одинаково, т.е. изменять значение, хранимое в контейнере. Если вместо std::vector использовать std::vector, то auto позволит менять только локальную переменную, а auto&& (как и auto&) уже само значение в контейнере.

Ответ 3



В первом случае идет перебор по значению, т.е. Вы не можете изменять элементы. Во втором случае - по универсальной ссылке. т.е. во втором случае можно писать код вида: for (auto&& i : container){ ++i;}

Ответ 4



Разницы никакой. Универсальная ссылка в цикле for используется чтобы код был совместим с контейнерами, итератор которых возвращает rvalue ссылки. template void interate1(C& container) //Non-const { for(auto& v: container) { //Do something with v } } template void iterate2(C& conatiner) { for(auto&& v: container) { //Do soemthign with v } } ... std::vector v(10); iterate1(v); //Error iterate2(v); //OK

Комментариев нет:

Отправить комментарий