Из функций (обычные ли они, или функции-члены класса) возвращают объекты указанного к функции типа. Иногда возвращают неполный тип: void. В первые дни изучения я часто возвращал из функций тип void только потому что не понимал, зачем нужен return. Со временем понимание, конечно, пришло. Возвращаемый тип нужен для того, чтобы процессом работы некоторой функции был порождён конечный объект, тип которому и будет определён по типу, заданному для функции.
Одна из неясностей, возникающей, наверное, почти у всех новичков, изучающих С++, связана с возвратом обычного типа и ссылки. Но обо всём по порядку.
В целом возможны следующие ситуации:
Возврат ссылки на объект
C++
1
int&foo(){return...}
Возврат константной ссылки на объект
C++
1
constint&foo(){return...}
Возврат объекта
C++
1
intfoo(){return...}
Возврат константного объекта
C++
1
2
3
constMyClass foo(){
returnMyClass();
}
В зависимости от ситуации может потребоваться любой из этих вариантов возврата. Рассмотрим их поочереди.
Возврат константной ссылки излюбленный вариант возврата многими специалистами. Константы всегда хорошо, а константные ссылки ещё лучше. Если ничто не мешает использовать константы, то нужно их использовать. Когда мы возвращаем значение из функции, то возможно возвращать либо независимый объект, неявно порождённый функцией в результате её работы, либо не полагаться на создание дополнительного объекта и избегать затрат времени на копирование такого объекта.
Чтобы не порождать лишних копий и не тратить время на лишние копирования, надо использовать возвращение значения по ссылке.
При возвращении из функции (константной или неокнстантной) ссылки нужно учитывать три момента:
При возврате объекта вызывается конструктор копирования, а при возврате ссылки — нет.
при выполнении вызываемой функции ссылка должна указывать на существующий объект.
Одна из распространённых у новичков ошибок: создать объект локальный, автоматический, потом вернуть ссылку на такой объект, но вместе с завершением функции объект, который некоторое время мог быть привязан к ссылке, будет уничтожен. В конечном итоге ссылка может указывать на незадействованный программой участок памяти.
Для возврата константных объектов нужно возвращать, если ссылки, то константные ссылки:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
classMyClass{
intx;
public:
MyClass():x(100){}
};
constMyClass& foo(const MyClass object) //параметр как константный объект
{
return object;//Всё правильно, возвращаемый объект константный, тип функции обозначени константой: константная ссылка
}
MyClass& foo(const MyClass object) //параметр как константный объект
{
return object;//Ошибка, тип функции не обозначени константой, а возваращаемый объект константный
}
intmain(){
MyClassx;
}
Варианты с ссылками выбирают в случае, если работают с объектами некоторого класса. Если работают с обычными переменными, не объектами классов, то эффективнее использовать не ссылки.
Надеюсь, с возвращением константной ссылки понятно. Повышает производительность и нужна, если на отдачу предназначен константный объект. Хотя используется не только для отдачи константного, но и для отдачи неконстантного объекта. Если ничто не мешает использовать константную ссылку и работа происходит с объектами класса, то константная ссылка предпочтительнее обычной ссылки.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Листинг #y1
#include <iostream>
usingstd::cout;
int& foo(){
int x = 0;
returnx;
}
intmain(){
intx=foo()=100;//Как думаете, это нормально? Чтобы этого не было, нужна константная ссылка для возврата из foo
cout<<x<<'\n';
}
Теперь поговорим о возврате из функции обычной ссылки.
Два популярных примера возврата не константного объекта — перегрузка операции присваивания и перегрузка операции << для использования с cout. Первое делается по соображениям повышения производительности, а второе — при необходимости.
Операция присваивания может использоваться в виде цепочки:
C++
1
2
3
4
5
6
7
8
//Листинг #y2
classMyClass{};
intmain(){
MyClassx,y,z;
x=y=z;//цепочка присваиваний
}
Это выражение раскладывается на (x = y) = z. Имеется две операции присваивания и в результате каждой из них отдаётся некоторая порождённая операцией сущность. Идеологически в операцию присваивания заложено изменять состояние объекта: объект-приёмник (тот, в который что-то присваиватся) обновляет своё состояние, согласно данным объекта-источника (присваиваемого). Если операция объекта-приёмника изменяет состояние этого объекта-приёмника, то операция не может быть константной. (метод не может быть константным, если он изменяет состояние объекта). Но чтобы не создавалось лишних объектов и не происходило расточительной траты памяти и времени работы программы на копирования, вариант с ссылкой предпочтителен. Представьте, что на 10 присваиваний сверху произойдёт создание 10 дополнительных объектов и 10 раз будет копироваться то, что можно не копировать. Да, нельзя идеологически использовать для операции присваивания константную ссылку, но ссылку-то можно. Можно возвращать и не ссылку, но тогда будут происходить неоправданные процессы: генерация временного объекта, запуск конструктора копирования, запуск деструктора. Поэтому, с целью повышения производительности, возвращают ссылку.
Значение, возвращаемое operator<< (), также применяется для присваивания в виде цепочки:
C++
1
2
Stringsi("Good stuff");
cout<<si<<"is coming!";//цепочка <<
Нельзя копировать потоки, поэтому единственным вариантом для возвращаемого объекта остаётся ссылка. Ведь если будет создан временный поток, то один поток в другой скопироваться не сможет и программа просто не скомпилируется. Т. е. если при перегрузке операции << возвращать не ссылку, то программа не сможет скомпилироваться, потому что потоки копироваться не умеют.
Теперь очередь возвращение из функции значения.
Не всегда нам нужны ссылки и не всегда мы можем использовать ссылки. В таком случае используют возврат по значению или возврат по константному значению. Возврат по значению всем нам, наверное, лучше всего знаком, потому что с первых дней изучения мы наиболее часто употребляем (как новички) именно такой способ возврата. Конечно, не все, но огромная масса новичков, как я могу догадываться, делает именно так. Как уже упоминалось, возврат по значению всегда связан с созданием временного объекта и срабатыванием конструктора копирования: ведь чтобы временный объект инициализировать, используется конструктор копирования. Это накладные расходы, но если никуда от них не деться, то останется с этим смириться.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//Листинг #x2 Возврат по значению всегда связан с дополнительным срабатыванием конструктора копирования
cout<<"destructor worked, object destroyed\n";//аналогично для деструктора
}
};
MyClass foo(){//из-за вовзрата по значению
MyClass temp(temp);//2-й конструктор копирования
returntemp;//3-й конструктор копирований <-- произошло так
}
intmain(){
MyClassx(x);//1-й конструктор копирования
{
foo();
}
}
Не всегда возможно из функции отдать валидную ссылку. Как уже упоминалось, простейший вариант дестабилизации работы программы — возврат ссылки на невалидный для программы участок памяти:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Листинг #x1
#include <iostream>
usingstd::cout;
constint& foo(){
int x = 100;//x Локальная для foo, автоматическая переменная
returnx;//возврат ссылки влечёт с собой возникновение проблем
}
intmain(){
cout<<foo()<<'\n';//Что будет — неопределено
//ссылка связана с разрушенным объектом
//результат непрогнозируем
}
Несмотря на то, что #x1 может работать правильно, в действительности это бомба замедленного действия. Настоящее поведение программы непредсказуемо. Нельзя возвращать ссылку на именованный объект, созданный локально. Когда функция прекратит свою работу, созданный в функции объект будет разрушен и ссылка окажется связанной с незадействованным программой участком памяти. Это серьёзная ошибка.
Если возвращаемый объект является локальным для вызванной функции, он не должен возвращаться по ссылке, поскольку при завершении функции для него вызывается собственный деструктор. Таким образом, когда управление возвращается в вызвавшую функцию, объект, на который может указывать ссылка, уже не существует. В таком случае следует возвращать объект, а не ссылку. Как правило, в эту категорию попадают перегруженные арифметические операции.
В некоторых случаях возможно возникновение ситуации, где останется возвращать только объект по значению. Показанный чуть ниже пример не имеет ничего общего с настоящим программированием, он просто демонстрирует, что такие ситуации действительно могут быть.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//Листинг c1
#include <iostream>
usingstd::cout;
classMyClass{
intx;
public:
MyClass():x(100){}
voidshow()const{cout<<x<<'\n';}
constMyClass& operator+(const MyClass& object) {
x += object.x;
return*this;
}
};
//const MyClass& foo() { //не получится (1)
//MyClass& foo(){ //не получится (2)
//const MyClass foo(){ //не получится (3)
MyClass foo(){//ОК. Возвращаем объект по значению
constMyClassx;
returnx;
}
intmain(){
(foo()+foo()).show();
system("PAUSE");
}
Поизучаем листинг #c1.
1. Возвращение константной ссылки. Правилами С++ предписано, что для константных объектов можно вызывать только константные методы, но наш operator += неконстантный. Хотя технически можно сделать константным, идеологически это будет неправильно: += обычно изменяет состояние объекта. А поскольку неконстантные методы для константных объектов вызывать нельзя, мы получаем ошибку компиляции.
2. Возвращение обычной ссылки. Как и в случае с константной ссылкой, если бы это компилировалось, то результат работы программы был бы непредсказуем. Ссылка, возвращённая из функции, была бы связана с разрушенным объектом. Несмотря на то что объект, с которым связывается ссылка, неконстантный, это тоже не компилируется. Причина здесь весьма простая: нельзя сделать из константны неконстанту.
C++
1
2
3
inta=100;//Если a был бы порождён внутри функции
constint& foo = a;//А преднеазначением функции было бы отдавать константную ссылку
int& x = foo;//то это бы было невозможно: foo — псевдоним const int a, а изменяемую ссылку на неизменяемый объект делать нельзя (иначе мы получим фальшивую константу, способную меняться под влиянием неконстантной ссылки)
3. Возвращение константного объекта. Поскольку объект константный, то вызывать можно только его константные методы, поэтому попытка вызвать неконстантный метод константного объекта спровоцирует ошибку компиляции.
4. Возвращение по значению. Возвращение по значеннию обозначает, что будет создана копия объекта. Поскольку возвращаемый объект не ссылка, то создаётся копия объекта. А поскольку объект неконстантный, неконстантная ссылка. Запросы к неконстантным методам для такого вполне законны, поэтому вариант с возвратом по значению срабатывает без нареканий.
Теперь о константном объекте.
Вариант с возвращением константного объекта по значению нужен реже уже описанных. Оно нужно прежде всего тогда, когда копия не должна измениться. Иногда бывает возникновение ситуации,, когда остаётся возвращать только константный объект по значению. Эта потребность может возникнуть в ходе решения задачи. Но когда она возникнет, вы скорее всего сами это поймёте. Без надобности так делать не нужно.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//Листинг #b1 Возвращение константного объекта
#include <iostream>
usingstd::cout;
classMyClass{
public:
voidconst_method()const{
cout<<"const_method\n";
}
voidnonconst_method(){
cout<<"non const method\n";
}
};
MyClass foo1(){
constMyClass temp;//Возвращается объект по значению
returntemp;
}
constMyClass foo2(){//Возвращается константный объект по значению
constMyClass temp;
returntemp;
}
intmain(){
foo1().nonconst_method();//foo1 возвращает неконстантный объект. Любой метод доступен
foo1().const_method();
foo2().nonconst_method();//Вызов неконстантных методов запрещён, из функции возвращается константный объект
foo2().const_method();
}
Почему иногда важно предпочесть константы не константам? Константны всегда связаны с тем, что объекты, к которым они применяются, не должны изменять своё состояние. Выдаваемый функцией объект можно использовать, например вот таким образом:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Листинг #z1
#include <iostream>
usingstd::cout;
classMyClass{
public:
/* просто метод класса, сейчас не имеет значения, как он работает */
constMyClass& print() const {
cout << "C++";//выводим на экран "С++"
return*this;
}
};
MyClass foo(){
MyClassx;//локальный объект
returnx;//отдаём объект по значению
}
intmain(){
MyClass temp;
foo()=foo()=foo().print();//Думаете это нормально? Чтобы этого избежать, нужно возвращать константу
}
Когда мы используем вызов функции, то нам отдачей приходит зарождённый функцией объект или ссылка на объект (зависит от типа, указанного для функции). Поскольку вызов функции в конечном счёте обычный объект (или обычная ссылка), то и использовать вызов функции можно как если бы это был обычный объект. Отсюда такие возможности, какие показаны в листинге #z1, и приходят. Но листинг #z1 выглядит странно, не находите? Такие возможности скорее нежелательны. Уже так сложилось, что это скорее запутывает, чем помогает. Поэтому, чтобы пресекать подобные действия при написании кода, используют для возвращения константы.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Листинг #z2
#include <iostream>
usingstd::cout;
classMyClass{
public:
/* просто метод класса, сейчас не имеет значения, как он работает */
constMyClass& print() const {
cout << "C++";//выводим на экран "С++"
return*this;
}
};
constMyClass foo(){//Возвращаем константное значение
MyClassx;
returnx;
}
intmain(){
MyClass temp;
foo()=foo()=foo().print();//Теперь это не сработает, foo() — это константный объект, изменениям не подлежит, а значит присваивать в него ничего не выйдет
}
Люди склонны допускать ошибки, поэтому, если не возвращать, например, константную ссылку, можно перепутав операцию = с == спровоцировать неверную работу какой-нибудь программы. Разумеется, потенциал ошибки не ограничивается показываемой перепеутанностью.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//Листинг #w1
#include <iostream>
usingstd::cout;
usingstd::ostream;
classMyClass{
intx;
public:
MyClass(constintvalue):x(value){}
MyClass& operator+ (const MyClass& object){ //возврат по ссылке, чтобы избежать накладных расходов на дополнительный объект
x+= object.x;
return*this;
}
constbooloperator==(constMyClass& object) const{ //для встроенных типов эффективнее возврат по значению
if(x+y=res){//!!! Ошибка, вместо == использовано =
cout<<x<<" + "<<y<<" == "<<res<<'\n';
}else{
cout<<x<<" + "<<y<<" != "<<res<<'\n';
};
//возможно, ожидается "-5 + -5 == 0", но на экране "0 + -5 == 0"
//подозрительно
}
Зачастую программисты стараются быть оригинальными, а это может привести к нетривиальным ошибкам. Несмотря на то, что код #w1 достаточно мал, обнаружить в нём ошибку может быть немного затруднительно. Осложнено всё тем, что это не такая ошибка, которая вылезет сразу. А если это большой проект и ошибка не такая простая, но порождённая из-за ярого желания быть оригинальным? В общем, чтобы избегать подобного в зародыше, нужно использовать константы. Чтобы не дать такой проблеме возникнуть, можно научить компилятор не компилировать показанный код, достаточно внести всего одно изменение: возвращаемое значение из операции + сделать константной ссылкой. Поскольку сама операция + не предназначена для изменения состояния объекта, то константная ссылка для неё вполне естественно: ссылка потому что объект класса, а константная потому что объект под влиянием операции не изменяет своё состояние. Такое простое лечение. И вы сейчас прекрасно можете начинать осознавать важность константности.
Статьёй не охватывается весь спектр возвращаемости значений и показанные примеры не единственные, которые могут повлиять на предпочтительный выбор. Это стоит понимать.
Добавить комментарий