Эффективный и современный С++. 42 рекомендации по использованию С++11 и С++14 - [122]

Шрифт
Интервал

и типов, преобразуемых в >std::string (см. раздел 5.3). В то же время имеются типы аргументов, которые не могут быть переданы с помощью универсальной ссылки (см. раздел 5.8), и если клиент передаст аргументы некорректного типа, сообщения компилятора об ошибках могут быть приводящими в трепет (см. раздел 5.5).

Было бы неплохо, если бы имелся способ написания функций наподобие >addName, таких, чтобы lvalue копировались, rvalue перемещались, при этом (в исходном тексте и объектном коде) имелась бы только одна функция и при этом можно было избежать неприятностей, связанных с универсальными ссылками. И такой способ есть. Все, что от вас требуется, — забыть об одном из первых правил, с которыми вы познакомились как программист на С++. Это правило, гласящее, что следует избегать передачи пользовательских типов по значению. Для параметров наподобие >newName в функциях наподобие >addName передача по значению может быть вполне разумной стратегией.

Перед тем как начать выяснение, почему передача по значению может быть хорошим решением для >newName и >addName, посмотрим, как она может быть реализована:

>class Widget {

>public:

> void addName(std::string newName)        // lvalue или

> { names.push_back(std::move(newName)); } // rvalue;

>                                          // перемещаем его

>};

Единственной неочевидной частью этого кода является применение >std::move к параметру >newName. Обычно >std::move используется с rvalue-ссылками, но в данном случае мы знаем, что (1) >newName представляет собой объект, полностью независимый от того, что передает вызывающая функция, так что изменение >newName не влияет на вызывающую функцию, и (2) это последнее применение >newName, так что его перемещение никак не влияет на остальную часть функции.

Тот факт, что существует только одна функция >addName, поясняет, как мы избегаем дублирования кода — как исходного, так и объектного. Мы не используем универсальную ссылку, так что данный подход не ведет к увеличению заголовочных файлов, странным неприятностям или непонятным сообщениям об ошибках. Но что можно сказать об эффективности такого дизайна? Мы же выполняем передачу по значению. Не слишком ли она дорога?

В С++98 можно держать пари, что так и есть. Независимо от того, что передает вызывающая функция, параметр >newName создается с помощью копирующего конструктора. Однако в С++11 >newName будет создаваться с помощью копирующего конструирования только для lvalue. В случае rvalue этот объект создается с помощью перемещающего конструктора. Вот, взгляните:

>Widget w;

>…

>std::string name("Bart");

>w.addName(name);           // Вызов addName с lvalue

>…

>w.addName(name + "Jenne"); // Вызов addName с rvalue

>                           // (см. ниже)

В первом вызове >addName (при передаче >name) параметр >newName инициализируется значением lvalue. Поэтому объект >newName создается путем копирования, так же, как это было бы в С++98. Во втором вызове >newName инициализируется объектом >std::string, полученным в результате вызова оператора >operator+ для >std::string  (т.e. выполнения операции добавления). Этот объект представляет собой rvalue, и >newName, таким образом, создается перемещением.

Итак, lvalue копируются, а rvalue перемещаются, как мы и хотели. Здорово, правда? Здорово, но есть несколько моментов, которые следует иметь в виду. Для облегчения понимания вспомним три рассмотренные версии функции >addName:

>class Widget { // Подход 1: перегрузка для lvalue и rvalue

>public:

> void addName(const std::string& newName)

> { names.push_back(newName); }

> void addName(std::string&& newName)

> { names.push_back(std::move(newName)); }

> …

>private:

> std::vector names;

>};


>class Widget { // Подход 2: применение универсальной ссылки

>public:

> template

> void addName(T&& newName)

> { names.push_back(std::forward(newName)); }

>};


>class Widget { // Подход 3: передача по значению public:

> void addName(std::string newName)

> { names.push_back(std::move(newName)); }

>};

Я буду говорить о первых двух версиях как о “подходе с передачей ссылки”, поскольку они обе передают параметры по ссылке.

Вот два сценария вызова, которые мы рассмотрели:

>Widget w;

>…

>std::string name("Bart");

>w.addName(name); // Передача lvalue

>…

>w.addName(name + "Jenne"); // Передача rvalue

Давайте теперь рассмотрим стоимость (в операциях копирования и перемещения) добавления имени в >Widget для приведенных сценариев и каждой из трех рассмотренных реализаций >addName. Мы будем игнорировать оптимизирующие возможности компиляторов по удалению копирований и перемещений, поскольку такая оптимизация зависит от контекста и компилятора и с практической точки зрения на суть анализа не влияет.

• Перегрузка. Независимо от передачи lvalue или rvalue аргумент вызывающей функции связан со ссылкой по имени >newName. Это ничего не стоит в смысле операций копирования и перемещения. В перегрузке для lvalue >newName копируется в >Widget::names. В перегрузке для rvalue объект перемещается. Итоговая стоимость: одно копирование lvalue, одно перемещение для rvalue.

• Применение универсальной ссылки. Как и в случае перегрузки, аргумент вызывающей функции связан со ссылкой


Еще от автора Скотт Мейерс
Эффективное использование STL

В этой книге известный автор Скотт Мейерс раскрывает секреты настоящих мастеров, позволяющие добиться максимальной эффективности при работе с библиотекой STL.Во многих книгах описываются возможности STL, но только в этой рассказано о том, как работать с этой библиотекой. Каждый из 50 советов книги подкреплен анализом и убедительными примерами, поэтому читатель не только узнает, как решать ту или иную задачу, но и когда следует выбирать то или иное решение — и почему именно такое.


Как функции, не являющиеся методами, улучшают инкапсуляцию

Когда приходится инкапсулировать, то иногда лучше меньше, чем большеЯ начну со следующего утверждения: Если вы пишете функцию, которая может быть выполнена или как метод класса, или быть внешней по отношению к классу, Вы должны предпочесть ее реализацию без использования метода. Такое решение увеличивает инкапсуляцию класса. Когда Вы думаете об использовании инкапсуляции, Вы должны думать том, чтобы не использовать методы.Удивлены? Читайте дальше.


Рекомендуем почитать
Pro Git

Разработчику часто требуется много сторонних инструментов, чтобы создавать и поддерживать проект. Система Git — один из таких инструментов и используется для контроля промежуточных версий вашего приложения, позволяя вам исправлять ошибки, откатывать к старой версии, разрабатывать проект в команде и сливать его потом. В книге вы узнаете об основах работы с Git: установка, ключевые команды, gitHub и многое другое.В книге рассматриваются следующие темы:основы Git;ветвление в Git;Git на сервере;распределённый Git;GitHub;инструменты Git;настройка Git;Git и другие системы контроля версий.


Java 7

Рассмотрено все необходимое для разработки, компиляции, отладки и запуска приложений Java. Изложены практические приемы использования как традиционных, так и новейших конструкций объектно-ориентированного языка Java, графической библиотеки классов Swing, расширенной библиотеки Java 2D, работа со звуком, печать, способы русификации программ. Приведено полное описание нововведений Java SE 7: двоичная запись чисел, строковые варианты разветвлений, "ромбовидный оператор", NIO2, новые средства многопоточности и др.


MFC и OpenGL

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


Симуляция частичной специализации

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


Обработка событий в С++

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


Питон — модули, пакеты, классы, экземпляры

Python - объектно-ориентированный язык сверхвысокого уровня. Python, в отличии от Java, не требует исключительно объектной ориентированности, но классы в Python так просто изучить и так удобно использовать, что даже новые и неискушенные пользователи быстро переходят на ОО-подход.