C требуется оператор ошибка

Ошибка с else. Требуется оператор C++ Решение и ответ на вопрос 2177153

Vyacheslav0202

0 / 0 / 0

Регистрация: 20.01.2018

Сообщений: 8

1

20.01.2018, 12:15. Показов 14112. Ответов 6

Метки нет (Все метки)


Здравствуйте. Написал простейшую программу в VS2013 Ultimate на определение чётности/нечётности числа. Проблема с конструкцией else. Ошибка: требуется оператор. Подскажите, что не так в коде?

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream> 
using namespace std;
void main()
{
    setlocale(LC_ALL, "ru");
    int a;
    cout << "Введите число:" << endl;
    cin >> a;
    if (a % 2);
    {
        cout << "Ваше число чётное" << endl;
    }
    else;
    {
        cout << "Ваше число нечётное" << endl;
    }
    system("pause");
}

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь



0



1754 / 1346 / 1407

Регистрация: 28.10.2016

Сообщений: 4,267

20.01.2018, 12:16

2

Лучший ответ Сообщение было отмечено Vyacheslav0202 как решение

Решение

точку с запятой после if и else уберите



1



CopBuroJLoBa

62 / 50 / 39

Регистрация: 03.01.2017

Сообщений: 133

20.01.2018, 12:16

3

Лучший ответ Сообщение было отмечено Vyacheslav0202 как решение

Решение

C++
1
if (a % 2 == 0);



1



pain1262

6 / 6 / 7

Регистрация: 24.09.2016

Сообщений: 63

20.01.2018, 12:17

4

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream> 
using namespace std;
void main()
{
    setlocale(LC_ALL, "ru");
    int a;
    cout << "Введите число:" << endl;
    cin >> a;
    if (a % 2 == 0)
    {
        cout << "Ваше число чётное" << endl;
    }
    else
    {
        cout << "Ваше число нечётное" << endl;
    }
    system("pause");
}



1



0 / 0 / 0

Регистрация: 20.01.2018

Сообщений: 8

20.01.2018, 12:20

 [ТС]

5

Большое спасибо. Работает.



0



Hitoku

1754 / 1346 / 1407

Регистрация: 28.10.2016

Сообщений: 4,267

20.01.2018, 12:20

6

Для столь простого условия в принципе можно использовать тернарный оператор

C++
9
cout << (a % 2 == 0 ? "Чётноеn" : "Нечётноеn");



1



ValeryS

Модератор

Эксперт по электронике

8759 / 6549 / 887

Регистрация: 14.02.2011

Сообщений: 22,972

20.01.2018, 13:08

7

Цитата
Сообщение от Hitoku
Посмотреть сообщение

Для столь простого условия в принципе можно использовать тернарный оператор

а можно и так

C++
1
2
string arrStr[]={"чет","нечет"};
cout<<arrStr[a%2];



0



#include <iostream>
#include <string>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <map>

using namespace std;


struct SBLnode {
    string name;
    SBLnode *next;
    SBLnode * left, * right;
};

struct Queue {
    SBLnode * first, * last;
};

typedef SBLnode* BST;


struct SBL {
    Queue q;
    BST root;
};

void SBL_init (SBL& sbl) {

    sbl = NULL;

}

I keep getting the following error in GCC when compiling…

error: no match for ‘operator=’ (operand types are ‘SBL’ and ‘long int’)
  sbl = NULL;
      ^

This error basically is for the line sbl = NULL and it would be great if someone could explain to me exactly what that error actually means.

asked Mar 14, 2014 at 17:09

user1952811's user avatar

9

It can’t find the operator= for SBL &SBL::operator=(const long int &rhs). There is a better practice. One option is to use a pointer and set it to NULL. NULL evaluates to 0. There is no operator which assigns an int intriniscally to your SBL struct object.

Or define a const static instance of the struct with the initial values and then simply assign this value to your variable whenever you want to reset it.

For example:

static const struct SBL EmptyStruct;

This uses static initialization to set the initial values.

Then, in init you can write:

sbl = EmptyStruct;

Note: Have to compile with -fpermissive in gcc or set EmptyStruct = { }. The reason why you have to set -fpermissive is listed here for GCC 4.6. GCC 4.4 needs EmptyStruct = { }.

Here is your program running. Initially it prints «initial» twice and on the third time, it prints empty string. Meaning, it was set to nothing by the assignment in the init function.

int main() 
{   
    struct SBLnode initial;
    initial.name = "initial";
    struct Queue q;
    q.first = &initial;
    cout << q.first->name << endl;
    struct SBL testInit;
    testInit.q = q;
    SBL_init(testInit);
    cout << testInit.q.first->name << endl;

    return 0;
}

http://ideone.com/Ecm6I9

answered Mar 14, 2014 at 17:13

Engineer2021's user avatar

Engineer2021Engineer2021

3,2586 gold badges28 silver badges51 bronze badges

4

void SBL_init (SBL& sbl) {

    sbl = NULL;

}

Others have already pointed out why that line doesn’t compile. Perhaps I can suggest an alternative solution. Instead of providing an init function, why not give all of your structures constructors like so? Is there some reason that you can’t provide those? The operator= and copy constructor don’t need to be defined if shallow copying of pointers is what you want. Since nodes typically need to be moved around I’m guessing that a shallow copy is fine. You can certainly use the nullptr if using c++ 11 rather than 0. I’m not a big fan of the NULL macro and opinions often vary with regards to NULL.

struct SBL {
    SBL() : root(0) {}
    Queue q;
    BST root;
};

struct Queue {
    Queue() : first(0), last(0) {}
    SBLnode * first, * last;
};

answered Mar 14, 2014 at 17:33

shawn1874's user avatar

shawn1874shawn1874

1,2281 gold badge8 silver badges25 bronze badges

NULL is a macro which expands to the integer literal 0. There is no intrinsic or user-defined operator which can assign an integer to an object of type SBL.

It looks like you are treating sbl as a pointer; but it is not a pointer, it is a reference.

You probably wanted to write this instead:

void SBL_init (SBL& sbl) {
    sbl.root = NULL;
}

This initializes sbl by nulling out its member pointers.

As others have commented, nullptr is preferred in C++11:

void SBL_init (SBL& sbl) {
    sbl.root = nullptr;
}

answered Mar 14, 2014 at 17:13

Oktalist's user avatar

OktalistOktalist

14k3 gold badges46 silver badges63 bronze badges

1

This error means that operator= , which is a function, is not defined in struct SBL. It is required when you write

sbl = NULL;

Solution:

provide SBL& operator=( const long int& i); in struct SBL.

In fact I think that you would like something alike SBL& operator=( BST b):

struct SBL {
    Queue q;
    BST root;
    SBL& operator=( BST b) {
      root = b;
      return *this;
    }
};

answered Mar 14, 2014 at 17:12

4pie0's user avatar

4pie04pie0

29k9 gold badges81 silver badges118 bronze badges

1

It is trying to find an assignment operator that has the form

 SBL &SBL::operator=(const long int &rhs):#

and cannot find one.

I guess you were thinking about the pointers.

4pie0's user avatar

4pie0

29k9 gold badges81 silver badges118 bronze badges

answered Mar 14, 2014 at 17:12

Ed Heal's user avatar

Ed HealEd Heal

58.6k17 gold badges87 silver badges125 bronze badges

#include <iostream>
#include <string>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <map>

using namespace std;


struct SBLnode {
    string name;
    SBLnode *next;
    SBLnode * left, * right;
};

struct Queue {
    SBLnode * first, * last;
};

typedef SBLnode* BST;


struct SBL {
    Queue q;
    BST root;
};

void SBL_init (SBL& sbl) {

    sbl = NULL;

}

I keep getting the following error in GCC when compiling…

error: no match for ‘operator=’ (operand types are ‘SBL’ and ‘long int’)
  sbl = NULL;
      ^

This error basically is for the line sbl = NULL and it would be great if someone could explain to me exactly what that error actually means.

asked Mar 14, 2014 at 17:09

user1952811's user avatar

9

It can’t find the operator= for SBL &SBL::operator=(const long int &rhs). There is a better practice. One option is to use a pointer and set it to NULL. NULL evaluates to 0. There is no operator which assigns an int intriniscally to your SBL struct object.

Or define a const static instance of the struct with the initial values and then simply assign this value to your variable whenever you want to reset it.

For example:

static const struct SBL EmptyStruct;

This uses static initialization to set the initial values.

Then, in init you can write:

sbl = EmptyStruct;

Note: Have to compile with -fpermissive in gcc or set EmptyStruct = { }. The reason why you have to set -fpermissive is listed here for GCC 4.6. GCC 4.4 needs EmptyStruct = { }.

Here is your program running. Initially it prints «initial» twice and on the third time, it prints empty string. Meaning, it was set to nothing by the assignment in the init function.

int main() 
{   
    struct SBLnode initial;
    initial.name = "initial";
    struct Queue q;
    q.first = &initial;
    cout << q.first->name << endl;
    struct SBL testInit;
    testInit.q = q;
    SBL_init(testInit);
    cout << testInit.q.first->name << endl;

    return 0;
}

http://ideone.com/Ecm6I9

answered Mar 14, 2014 at 17:13

Engineer2021's user avatar

Engineer2021Engineer2021

3,2586 gold badges28 silver badges51 bronze badges

4

void SBL_init (SBL& sbl) {

    sbl = NULL;

}

Others have already pointed out why that line doesn’t compile. Perhaps I can suggest an alternative solution. Instead of providing an init function, why not give all of your structures constructors like so? Is there some reason that you can’t provide those? The operator= and copy constructor don’t need to be defined if shallow copying of pointers is what you want. Since nodes typically need to be moved around I’m guessing that a shallow copy is fine. You can certainly use the nullptr if using c++ 11 rather than 0. I’m not a big fan of the NULL macro and opinions often vary with regards to NULL.

struct SBL {
    SBL() : root(0) {}
    Queue q;
    BST root;
};

struct Queue {
    Queue() : first(0), last(0) {}
    SBLnode * first, * last;
};

answered Mar 14, 2014 at 17:33

shawn1874's user avatar

shawn1874shawn1874

1,2281 gold badge8 silver badges25 bronze badges

NULL is a macro which expands to the integer literal 0. There is no intrinsic or user-defined operator which can assign an integer to an object of type SBL.

It looks like you are treating sbl as a pointer; but it is not a pointer, it is a reference.

You probably wanted to write this instead:

void SBL_init (SBL& sbl) {
    sbl.root = NULL;
}

This initializes sbl by nulling out its member pointers.

As others have commented, nullptr is preferred in C++11:

void SBL_init (SBL& sbl) {
    sbl.root = nullptr;
}

answered Mar 14, 2014 at 17:13

Oktalist's user avatar

OktalistOktalist

14k3 gold badges46 silver badges63 bronze badges

1

This error means that operator= , which is a function, is not defined in struct SBL. It is required when you write

sbl = NULL;

Solution:

provide SBL& operator=( const long int& i); in struct SBL.

In fact I think that you would like something alike SBL& operator=( BST b):

struct SBL {
    Queue q;
    BST root;
    SBL& operator=( BST b) {
      root = b;
      return *this;
    }
};

answered Mar 14, 2014 at 17:12

4pie0's user avatar

4pie04pie0

29k9 gold badges81 silver badges118 bronze badges

1

It is trying to find an assignment operator that has the form

 SBL &SBL::operator=(const long int &rhs):#

and cannot find one.

I guess you were thinking about the pointers.

4pie0's user avatar

4pie0

29k9 gold badges81 silver badges118 bronze badges

answered Mar 14, 2014 at 17:12

Ed Heal's user avatar

Ed HealEd Heal

58.6k17 gold badges87 silver badges125 bronze badges

Содержание

  • 1 Идея структурного программирования
  • 2 Оператор «выражение»
  • 3 Операторы ветвления
    • 3.1 Условный оператор if
    • 3.2 Оператор switch
  • 4 Операторы цикла
    • 4.1 Цикл с предусловием (while)
    • 4.2 Цикл с постусловием (do while)
    • 4.3 Цикл с параметром (for)
  • 5 Операторы передачи управления
    • 5.1 Оператор goto
    • 5.2 Оператор break
    • 5.3 Оператор continue
    • 5.4 Оператор return

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

Следование, ветвление и цикл называют базовыми конструкциями структурного программирования.

Идея структурного программирования

Как уже было отмечено выше, любую программу можно составить только из структур трех типов: следования, ветвления и цикла (это базовые конструкции).

  • Следованием называется конструкция, представляющая собой последовательное выполнение двух или более операторов (простых или составных).
  • Ветвление задает выполнение либо одного, либо другого оператора в зависимости от выполнения какого-либо условия.
  • Цикл задает многократное выполнение оператора.

Базовые конструкции структурного программирования

Базовые конструкции структурного программирования

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

Вложение базовых конструкций

Вложение базовых конструкций

Идеей использования базовых конструкций является получение программы простой структуры. Такую программу легко читать (а программы чаще приходится читать, чем писать), отлаживать и при необходимости вносить в нее изменения. Структурное профаммирование часто называли «программированием без goto», и в этом есть большая доля правды: частое использование операторов передачи управления в произвольные точки программы затрудняет прослеживание логики ее работы. С другой стороны, никакие принципы нельзя возводить в абсолют, и есть ситуации, в которых использование goto оправдано и приводит, напротив, к упрощению структуры программы.

В большинстве языков высокого уровня существует несколько реализаций базовых конструкций; в C++ есть три вида циклов и два вида ветвлений (на два и на произвольное количество направлений). Они введены для удобства программирования, и в каждом случае надо выбирать наиболее подходящие средства. Главное, о чем нужно помнить даже при написании самых простых программ, — что они должны состоять из четкой последовательности блоков строго определенной конфигурации. «Кто ясно мыслит, тот ясно излагает» — практика давно показала, что программы в стиле «поток сознания» нежизнеспособны, не говоря о том, что они просто некрасивы.

Рассмотрим операторы языка, реализующие базовые конструкции структурного программирования в C++.

Оператор «выражение»

Любое выражение, завершающееся точкой с запятой, рассматривается как оператор, выполнение которого заключается в вычислении выражения. Частным случаем выражения является пустой оператор ; (он используется, когда по синтаксису оператор требуется, а по смыслу — нет). Примеры:

i++; // выполняется операция инкремента
а* = b + с; // выполняется умножение с присваиванием
fun(i, к); // выполняется вызов функции

Операторы ветвления

Условный оператор if

Условный оператор if используется для разветвления процесса вычислений на два направления. Формат оператора:

if ( выражение ) оператор_1; [else оператор_2;]

Сначала вычисляется выражение, которое может иметь арифметический тип или тип указателя. Если оно не равно нулю (имеет значение true), выполняется первый оператор, иначе — второй. После этого управление передается на оператор, следующий за условным.

Одна из ветвей может отсутствовать, логичнее опускать вторую ветвь вместе с ключевым словом else. Если в какой-либо ветви требуется выполнить несколько операторов, их необходимо заключить в блок, иначе компилятор не сможет понять, где заканчивается ветвление. Блок может содержать любые операторы, в том числе описания и другие условные операторы (но не может состоять из одних описаний). Необходимо учитывать, что переменная, описанная в блоке, вне блока не существует.

Структурная схема условного оператора

Структурная схема условного оператора

Примеры:
if (а<0) b = 1; //1
if (a<b && (a>d || a==0)) b++; else {b *= a; a = 0;} //2
if (a<b) {if (a<c) m = a; else m = c;} else {if (b<c) m = b; else m = c;} // 3
if (a++) b++; // 4
if (b>a) max = b; else max = a; //5

В примере 1 отсутствует ветвь else. Подобная конструкция называется «пропуск оператора», поскольку присваивание либо выполняется, либо пропускается в зависимости от выполнения условия.

Если требуется проверить несколько условий, их объединяют знаками логических операций. Например, выражение в примере 2 будет истинно в том случае, если выполнится одновременно условие а<Ь и одно из условий в скобках. Если опустить внутренние скобки, будет выполнено сначала логическое И, а потом — ИЛИ.

Оператор в примере 3 вычисляет наименьшее значение из трех переменных. Фигурные скобки в данном случае не обязательны, так как компилятор относит часть else к ближайшему if.

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

Конструкции, подобные оператору в примере 5, пройде и нагляднее записывать в виде условной операции (в данном случае: max = (b > а) ? b : а;).

Пример. Производится выстрел по мишени, изображенной на рис. ниже. Определить количество очков.

Мишень
#include <iostream.h>
int main(){
float x, у: int kol;
cout << «Введите координаты выстрелаn»; cin >> x >> у;
if ( x*x + y*y < 1 ) kol = 2;
else if( x*x + y*y < 4 ) kol = 1:
else коl = 0;
cout << «n Очков: » << kol;
return 0;
}

Базовые конструкции структурного программирования в C++Внимание

Распространенная ошибка при записи условных операторов — использование в выражениях вместо проверки на равенство (==) простого присваивания (=), например if(a=1)b=0;. Синтаксической ошибки нет, так как операция присваивания формирует результат, который и оценивается на равенство/неравенство нулю. В данном примере присваивание переменной b будет выполнено независимо от значения переменной а. Поэтому в выражениях проверки переменной на равенство константе константу рекомендуется записывать слева от операции сравнения: if (1==а) Ь=0[ccie_cpp];.

Вторая ошибка — неверная запись проверки на принадлежность диапазону. Например, чтобы проверить условие 0<х<1, нельзя записать его в условном операторе непосредственно, так как будет выполнено сначала сравнение 0<х, а его результат (true или false, преобразованное в int) будет сравниваться с 1. Правильный способ записи: [ccie_cpp]if(0<x && х<1)…

Тип переменных выбирается исходя из их назначения. Координаты выстрела нельзя представить целыми величинами, так как это приведет к потере точности результата, а счетчик очков не имеет смысла описывать как вещественный. Даже такую простую программу можно еще упростить с помощью промежуточной переменной и записи условия в виде двух последовательных, а не вложенных, операторов if (обратите внимание, что в первом варианте значение переменной коl присваивается ровно один раз, а во втором — от одного до трех раз в зависимости от выполнения условий):

#1nc!ucle
int main(){
float x, у, temp; int коl;
cout << «Введите координаты выстрелаn»; cin >> X >> у;
temp = x * x + y * y;
коl = 0;
if (temp < 4 ) коl = 1;
if (temp < 1 ) kol = 2:
cout << «n Очков: » << kol;
return 0;
}

Если какая-либо переменная используется только внутри условного оператора, рекомендуется объявить ее внутри скобок, например:

if (int i = fun(t)) a -= 1; else a += 1;

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

Оператор switch

Оператор switch (переключатель) предназначен для разветвления процесса вычислений на несколько направлений. Структурная схема оператора приведена на рис. ниже. Формат оператора:

switch ( выражение ) {
case константное_выражение_1: [список_операторов_1]
case константное_выражение_2: [список_операторов_2]

case константное_выражение_n: [список_операторов_n]
[default: операторы ]
}

Структурная схема оператора switchВыполнение оператора начинается с вычисления выражения (оно должно быть целочисленным), а затем управление передается первому оператору из списка, помеченного константным выражением, значение которого совпало с вычисленным. После этого, если выход из переключателя явно не указан, последовательно выполняются все остальные ветви.

Выход из переключателя обычно выполняется с помощью операторов break или return. Оператор break выполняет выход из самого внутреннего из объемлющих его операторов switch, for, while и do. Оператор return выполняет выход из функции, в теле которой он записан.

Все константные выражения должны иметь разные значения, но быть одного и того же целочисленного типа. Несколько меток могут следовать подряд. Если совпадения не произошло, выполняются операторы, расположенные после слова default (а при его отсутствии управление передается следующему за switch оператору).

Пример (программа реализует простейший калькулятор на 4 действия):

#include
int main() {
int a, b, res;
char op;
cout << «nВведите 1й операнд : «; cin >> a;
cout << «nВведите знак операции : «; cin >> op;
cout << «nВведите 2й операнд : «; cin >> b;
bool f = true;

switch (op){
case ‘+’: res = a + b; break;
case ‘-‘: res = a — b; break;
case ‘*’: res = a * b; break;
case ‘/’: res = a / b; break;
default : «nНеизвестная операция»; f = false;
}

if (f) cout << «nРезультат: » << res;
return 0;
}

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

Операторы цикла

Операторы цикла используются для организации многократно повторяющихся вычислений. Любой цикл состоит из тела цикла, то есть тех операторов, которые выполняются несколько раз, начальных установок, модификации параметра цикла и проверки условия продолжения выполнения цикла.

Структурные схемы операторов циклаОдин проход цикла называется итерацией. Проверка условия выполняется на каждой итерации либо до тела цикла (тогда говорят о цикле с предусловием), либо после тела цикла (цикл с постусловием). Разница между ними состоит в том, что тело цикла с постусловием всегда выполняется хотя бы один раз, после чего проверяется, надо ли его выполнять еще раз. Проверка необходимости выполнения цикла с предусловием делается до тела цикла, поэтому возможно, что он не выполнится ни разу.

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

Начальные установки могут явно не присутствовать в программе, их смысл состоит в том, чтобы до входа в цикл задать значения переменным, которые в нем используются.

Цикл завершается, если условие его продолжения не выполняется. Возможно принудительное завершение как текущей итерации, так и цикла в целом. Для этого служат операторы break, continue, return и goto. Передавать управление извне внутрь цикла не рекомендуется.

Для удобства, а не по необходимости, в C++ есть три разных оператора цикла — while, do while и for.

Цикл с предусловием (while)

Цикл с предусловием реализует структурную схему, приведенную на рис. выше, и имеет вид:

while ( выражение ) оператор

Выражение определяет условие повторения тела цикла, представленного простым или составным оператором. Выполнение оператора начинается с вычисления выражения. Если оно истинно (не равно false), выполняется оператор цикла. Если при первой проверке выражение равно false, цикл не выполнится ни разу. Тип выражения должен быть арифметическим или приводимым к нему. Выражение вычисляется перед каждой итерацией цикла.

Пример (программа печатает таблицу значений функции y = х2+1 во введенном диапазоне):

#include
int main(){
float Xn, Xk, Dx;
printf(«Введите диапазон и шаг изменения аргумента: » );
scanf(«%f%f%f», &Хn, &Хк, &Dx);
printf(» | X | Y |n»); // шапка таблицы
float X = Xn; // установка параметра цикла
while (X printf(«| %5.2f | %5.2f |n», X, X*X + 1 ); // тело цикла
X += Dx; // модификация параметра
}
return 0;
}

Распространенный прием программирования — организация бесконечного цикла с заголовком while (true) либо while (1) и принудительным выходом из тела цикла по выполнению какого-либо условия.

В круглых скобках после ключевого слова while можно вводить описание переменной. Областью ее действия является цикл:

while (int х = 0) { … / * область действия х */ }

Цикл с постусловием (do while)

Цикл с постусловием реализует структурную схему, приведенную на рис. выше, б, и имеет вид:

do оператор while выражение;

Сначала выполняется простой или составной оператор, составляющий тело цикла, а затем вычисляется выражение. Если оно истинно (не равно f а! se), тело цикла выполняется еще раз. Цикл завершается, когда выражение станет равным false или в теле цикла будет выполнен какой-либо оператор передачи управления. Тип выражения должен быть арифметическим или приводимым к нему.

Пример (программа осуществляет проверку ввода):

#include
int main(){
char answer;
do{
cout << «nКупи слоника! «; cin >> answer;
} while (answer != ‘y’);
return 0;
}

Цикл с параметром (for)

Цикл с параметром имеет следующий формат:

for (инициализация; выражение; модификации) оператор;

Инициализация используется для объявления и присвоения начальных значений величинам, используемым в цикле. В этой части можно записать несколько операторов, разделенных запятой (операцией «последовательное выполнение»), например, так:

for (int 1 = 0, j = 2; …
int к. m;
for (k = 1, m = 0; …

Областью действия переменных, объявленных в части инициализации цикла, является цикл. Инициализация выполняется один раз в начале исполнения цикла.

Выражение определяет условие выполнения цикла: если его результат, приведенный к типу bool, равен true, цикл выполняется. Цикл с параметром реализован  как цикл с предусловием.

Модификации выполняются после каждой итерации цикла и служат обычно для изменения параметров цикла. В части модификаций можно записать несколько операторов через запятую. Простой или составной оператор представляет собой тело цикла. Любая из частей оператора for может быть опущена (но точки с запятой надо оставить на своих местах!).

Пример (оператор, вычисляющий сумму чисел от 1 до 100):

for (int 1 = 1, s = 0; 1<=100; 1++) s += 1;

Часто встречающиеся ошибки при программировании циклов — использование в теле цикла неинициализированных переменных и неверная запись условия выхода из цикла.

Чтобы избежать ошибок, рекомендуется:

  • проверить, всем ли переменным, встречающимся в правой части операторов присваивания в теле цикла, присвоены до этого начальные значения (а также возможно ли выполнение других операторов);
  • проверить, изменяется ли в цикле хотя бы одна переменная, входящая в условие выхода из цикла;
  • предусмотреть аварийный выход из цикла по достижению некоторого количества итераций;
  • и, конечно, не забывать о том, что если в теле цикла требуется выполнить более одного оператора, нужно заключать их в фигурные скобки.

Операторы цикла взаимозаменяемы, но можно привести некоторые рекомендации по выбору наилучшего в каждом конкретном случае.

Оператор do while обычно используют, когда цикл требуется обязательно выполнить хотя бы раз (например, если в цикле производится ввод данных).

Оператором while удобнее пользоваться в случаях, когда число итераций заранее не известно, очевидных параметров цикла нет или модификацию параметров удобнее записывать не в конце тела цикла.

Оператор for предпочтительнее в большинстве остальных случаев (однозначно — для организации циклов со счетчиками).

Операторы передачи управления

В C++ есть четыре оператора, изменяющих естественный порядок выполнения вычислений:

  • оператор безусловного перехода goto;
  • оператор выхода из цикла break;
  • оператор перехода к следующей итерации цикла continue;
  • оператор возврата из функции return.

Оператор goto

Оператор безусловного перехода goto имеет формат:

goto метка;

В теле той же функции должна присутствовать ровно одна конструкция вида:

метка: оператор;

Оператор goto передает управление на помеченный оператор. Метка — это обычный идентификатор, областью видимости которого является функция, в теле которой он задан.

Использование оператора безусловного перехода оправдано в двух случаях:

  • принудительный выход вниз по тексту программы из нескольких вложенных циклов или переключателей;
  • переход из нескольких мест функции в одно (например, если перед выходом из функции всегда необходимо выполнять какие-либо действия).

В остальных случаях для записи любого алгоритма существуют более подходящие средства, а использование goto приводит только к усложнению структуры программы и затруднению отладки (Даже в приведенных случаях допустимо применять goto только в случае, если в этих фрагментах кода не создаются локальные объекты. В противном случае возможно применение деструктора при пропущенгюм конструкторе, что приводит к серьезным ошибкам в программе). Применение goto нарушает принципы структурного и модульного программирования, по которым все блоки, из которых состоит программа, должны иметь только один вход и один выход.

В любом случае не следует передавать управление внутрь операторов if, switch и циклов. Нельзя переходить внутрь блоков, содержащих инициализацию переменных, на операторы, расположенные после нее, поскольку в этом случае инициализация не будет выполнена:

int к; …
goto metka; …
{int а = 3, b = 4;
к = а + b;
metka: int m = к + 1; …
}

После выполнения этого фрагмента программы значение переменной m не определено.

Оператор break

Оператор break используется внутри операторов цикла или switch для обеспечения перехода в точку программы, находящуюся непосредственно за оператором, внутри которого находится break.

Оператор continue

Оператор перехода к следующей итерации цикла continue пропускает все операторы, оставшиеся до конца тела цикла, и передает управление на начало следующей итерации.

Оператор return

Оператор возврата из функции return завершает выполнение функции и передает управление в точку ее вызова. Вид оператора:

return [ выражение ];

Выражение должно иметь скалярный тип. Если тип возвращаемого функцией значения описан как void, выражение должно отсутствовать.

По материалам книги Т.А. Павловской «CC++. Программирование на языке высокого уровня»

На чтение 6 мин. Просмотров 125 Опубликовано 15.12.2019

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

К примеру, мы вводим с клавиатуры целое число. Если это число больше десяти, то программа должна выполнить одно действие, иначе — другое. Реализуем этот алгоритм на C++ с помощью конструкции ветвления.

Если вы запустите эту программу, то при вводе числа, меньшего десяти, будет выводиться соответствующее сообщение.

Если введенное число окажется большим, либо равным десяти — отобразится другое сообщение.

Содержание

  1. Оператор if
  2. Пример конструкции ветвления
  3. Что такое оператор ветвления?
  4. Оператор if
  5. Синтаксис Syntax
  6. Пример Example
  7. Оператор If с инициализатором if statement with an initializer
  8. Пример Example
  9. If constexpr, операторы if constexpr statements

Оператор if

Оператор if служит для того, чтобы выполнить какую-либо операцию в том случае, когда условие является верным. Условная конструкция в С++ всегда записывается в круглых скобках после оператора if .

Внутри фигурных скобок указывается тело условия. Если условие выполнится, то начнется выполнение всех команд, которые находятся между фигурными скобками.

Пример конструкции ветвления

Здесь говорится: «Если переменная num меньше 10 — вывести соответствующее сообщение. Иначе, вывести другое сообщение».

Усовершенствуем программу так, чтобы она выводила сообщение, о том, что переменная num равна десяти:

Здесь мы проверяем три условия:

  • Первое — когда введенное число меньше 10-ти
  • Второе — когда число равно 10-ти
  • И третье — когда число больше десяти

Заметьте, что во втором условии, при проверке равенства, мы используем оператор равенства — == , а не оператор присваивания, потому что мы не изменяем значение переменной при проверке, а сравниваем ее текущее значение с числом 10.

  • Если поставить оператор присваивания в условии, то при проверке условия, значение переменной изменится, после чего это условие выполнится.

Каждому оператору if соответствует только один оператор else . Совокупность этих операторов — else if означает, что если не выполнилось предыдущее условие, то проверить данное. Если ни одно из условий не верно, то выполняется тело оператора else .

Если после оператора if , else или их связки else if должна выполняться только одна команда, то фигурные скобки можно не ставить. Предыдущую программу можно записать следующим образом:

Такой метод записи выглядит более компактно. Если при выполнении условия нам требуется выполнить более одной команды, то фигурные скобки необходимы. Например:

Данная программа проверяет значение переменной num. Если она меньше 10, то присваивает переменной k значение единицы. Если переменная num равна десяти, то присваивает переменной k значение двойки. В противном случае — значение тройки. После выполнения ветвления, значение переменной k выводится на экран.

Хорошенько потренируйтесь, попробуйте придумать свой пример с ветвлением. Когда поймете, что поняли эту тему, можете смело приступать к следующему уроку.

Здравствуйте, дорогие друзья! Сложно представить себе полноценную программу, которая будет работать одинаково при любых обстоятельствах. Довольно часто приходится выбирать, между несколькими вариантами развития событий в зависимости от поступающих данных. Решением данной проблемы в C++ занимается оператор ветвления.

Что такое оператор ветвления?

Давайте представим, что мы пришли в магазин. У нас есть одна цель — купить товар. Однако есть одно ограничение. Его цена не должна превышать определенную сумму. В нашем случае условие следующие: our_money >= price . Если это условие выполняется, то у нас все замечательно и мы счастливые направляемся к кассе. В противном случае нам придется искать более дешевый вариант данного товара.

Думаю у вас в голове уже сложилось понимание основных концептов ветвления в C++.

Оператор if

Чтобы указать условие при выполнении которого будет выполняться определенный код, тогда вам потребуется прибегнуть к следующей структуре:

  • набрать ключевое слово if в своей программе;
  • после него, в круглых скобках, вы должны указать логическое выражение:

Управляет условным ветвлением. Controls conditional branching. Операторы в блоке if выполняются только в том случае, если параметр -Expression имеет ненулевое значение (или true). Statements in the if-block are executed only if the if-expression evaluates to a non-zero value (or TRUE). Если значение Expression не равно нулю, то оператор1 и все другие операторы в блоке выполняются, а else-Block, если он есть, пропускается. If the value of expression is nonzero, statement1 and any other statements in the block are executed and the else-block, if present, is skipped. Если значение Expression равно нулю, то параметр if-Block пропускается и выполняется else-Block, если он есть. If the value of expression is zero, then the if-block is skipped and the else-block, if present, is executed. Выражения, результатом которых является ненулевое значение, являются Expressions that evaluate to non-zero are

  • true TRUE
  • указатель, отличный от NULL, a non-null pointer,
  • любое ненулевое арифметическое значение или any non-zero arithmetic value, or
  • тип класса, определяющий однозначное преобразование к арифметическому, логическому или типу указателя. a class type that defines an unambiguous conversion to an arithmetic, boolean or pointer type. (Дополнительные сведения о преобразованиях см. в разделе стандартные преобразования.) (For information about conversions, see Standard Conversions.)

Синтаксис Syntax

Пример Example

Оператор If с инициализатором if statement with an initializer

Visual Studio 2017 версии 15,3 и более поздних версий (доступно в /std: c++ 17): Оператор If может также содержать выражение, которое объявляет и инициализирует именованную переменную. Visual Studio 2017 version 15.3 and later (available with /std:c++17): An if statement may also contain an expression that declares and initializes a named variable. Используйте эту форму оператора if, если переменная необходима только в области видимости блока if. Use this form of the if-statement when the variable is only needed within the scope of the if-block.

Пример Example

Во всех формах оператора If выражение, которое может иметь любое значение, кроме структуры, вычисляется, включая все побочные эффекты. In all forms of the if statement, expression, which can have any value except a structure, is evaluated, including all side effects. Управление передается из оператора If в следующий оператор в программе, если только одна из инструкцийне содержит прерывание, Continueили goto. Control passes from the if statement to the next statement in the program unless one of the statements contains a break, continue, or goto.

Предложение if. else else оператора связано с ближайшим предыдущим оператором if в той же области, которая не имеет соответствующей инструкции else . The else clause of an if. else statement is associated with the closest previous if statement in the same scope that does not have a corresponding else statement.

If constexpr, операторы if constexpr statements

Visual Studio 2017 версии 15,3 и более поздних версий (доступно в /std: c++ 17): В шаблонах функций можно использовать оператор If constexpr , чтобы принимать решения о ветвлении во время компиляции без необходимости прибегать к нескольким перегрузкам функций. Visual Studio 2017 version 15.3 and later (available with /std:c++17): In function templates, you can use an if constexpr statement to make compile-time branching decisions without having to resort to multiple function overloads. Например, можно написать одну функцию, которая обрабатывает распаковку параметров (без перегрузки нулевого параметра): For example, you can write a single function that handles parameter unpacking (no zero-parameter overload is needed):

Добавлено 30 мая 2021 в 17:27

В уроке «3.1 – Синтаксические и семантические ошибки» мы рассмотрели синтаксические ошибки, которые возникают, когда вы пишете код, который не соответствует грамматике языка C++. Компилятор уведомит вас об ошибках этого типа, поэтому их легко обнаружить и обычно легко исправить.

Мы также рассмотрели семантические ошибки, которые возникают, когда вы пишете код, который выполняет не то, что вы планировали. Как правило, компилятор не обнаруживает семантических ошибок (хотя в некоторых случаях умные компиляторы могут генерировать предупреждения).

Семантические ошибки могут вызывать большинство из симптомов неопределенного поведения, например, приводить к тому, что программа выдает неправильные результаты, быть причиной неустойчивого поведения, искажать данные программы, вызывать сбой программы – или они могут вообще никак не влиять.

При написании программ семантические ошибки практически неизбежны. Некоторые из них вы, вероятно, заметите, просто используя программу: например, если вы писали игру лабиринт, а ваш персонаж может проходить сквозь стены. Тестирование вашей программы (7.12 – Введение в тестирование кода) также может помочь выявить семантические ошибки.

Но есть еще одна вещь, которая может помочь – это знание того, какой тип семантических ошибок наиболее распространен, чтобы вы могли потратить немного больше времени на то, чтобы убедиться, что в этих случаях всё правильно.

В этом уроке мы рассмотрим ряд наиболее распространенных типов семантических ошибок, возникающих в C++ (большинство из которых так или иначе связаны с управлением порядком выполнения программы).

Условные логические ошибки

Один из наиболее распространенных типов семантических ошибок – это условная логическая ошибка. Условная логическая ошибка возникает, когда программист неправильно пишет логику условного оператора или условия цикла. Вот простой пример:

#include <iostream>
 
int main()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;
 
    if (x >= 5) // упс, мы использовали operator>= вместо operator>
        std::cout << x << " is greater than 5";
 
    return 0;
}

А вот результат запуска программы, при котором была обнаружена эта условная логическая ошибка:

Enter an integer: 5
5 is greater than 5

Когда пользователь вводит 5, условное выражение x >= 5 принимает значение true, поэтому выполняется соответствующая инструкция.

Вот еще один пример для цикла for:

#include <iostream>
 
int main()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;
 
    // упс, мы использовали operator> вместо operator<
    for (unsigned int count{ 1 }; count > x; ++count)
    {
        std::cout << count << ' ';
    }
 
    return 0;
}

Эта программа должна напечатать все числа от 1 до числа, введенного пользователем. Но вот что она на самом деле делает:

Enter an integer: 5

Она ничего не напечатала. Это происходит потому, что при входе в цикл for условие count > x равно false, поэтому цикл вообще не повторяется.

Бесконечные циклы

В уроке «7.7 – Введение в циклы и инструкции while» мы рассмотрели бесконечные циклы и показали этот пример:

#include <iostream>
 
int main()
{
    int count{ 1 };
    while (count <= 10) // это условие никогда не будет ложным
    {
        std::cout << count << ' '; // поэтому эта строка выполняется многократно
    }
 
    return 0; // эта строка никогда не будет выполнена
}

В этом случае мы забыли увеличить count, поэтому условие цикла никогда не будет ложным, и цикл продолжит печатать:

1 1 1 1 1 1 1 1 1 1

пока пользователь не закроет программу.

Вот еще один пример, который преподаватели любят задавать в тестах. Что не так со следующим кодом?

#include <iostream>
 
int main()
{
    for (unsigned int count{ 5 }; count >= 0; --count)
    {
        if (count == 0)
            std::cout << "blastoff! ";
        else
          std::cout << count << ' ';
    }
 
    return 0;
}

Эта программа должна напечатать «5 4 3 2 1 blastoff!«, что она и делает, но не останавливается на достигнутом. На самом деле она печатает:

5 4 3 2 1 blastoff! 4294967295 4294967294 4294967293 4294967292 4294967291

а затем просто продолжает печатать уменьшающиеся числа. Программа никогда не завершится, потому что условие count >= 0 никогда не может быть ложным, если count является целым числом без знака.

Ошибки на единицу

Ошибки «на единицу» возникают, когда цикл повторяется на один раз больше или на один раз меньше, чем это необходимо. Вот пример, который мы рассмотрели в уроке «7.9 – Инструкции for»:

#include <iostream>
 
int main()
{
    for (unsigned int count{ 1 }; count < 5; ++count)
    {
        std::cout << count << ' ';
    }
 
    return 0;
}

Этот код должен печатать «1 2 3 4 5«, но он печатает только «1 2 3 4«, потому что был использован неправильный оператор отношения.

Неправильный приоритет операторов

Следующая программа из урока «5.7 – Логические операторы» допускает ошибку приоритета операторов:

#include <iostream>
 
int main()
{
    int x{ 5 };
    int y{ 7 };
 
    if (!x > y)
        std::cout << x << " is not greater than " << y << 'n';
    else
        std::cout << x << " is greater than " << y << 'n';
 
    return 0;
}

Поскольку логическое НЕ имеет более высокий приоритет, чем operator>, условное выражение вычисляется так, как если бы оно было написано (!x) > y, что не соответствует замыслу программиста.

В результате эта программа печатает:

5 is greater than 7

Это также может произойти при смешивании в одном выражении логического ИЛИ и логического И (логическое И имеет больший приоритет, чем логическое ИЛИ). Используйте явные скобки, чтобы избежать подобных ошибок.

Проблемы точности с типами с плавающей запятой

Следующая переменная с плавающей запятой не имеет достаточной точности для хранения всего числа:

#include <iostream>
 
int main()
{
    float f{ 0.123456789f };
    std::cout << f;
}

Как следствие, эта программа напечатает:

0.123457

В уроке «5.6 – Операторы отношения и сравнение чисел с плавающей запятой» мы говорили о том, что использование operator== и operator!= может вызывать проблемы с числами с плавающей запятой из-за небольших ошибок округления (а также о том, что с этим делать). Вот пример:

#include <iostream>
 
int main()
{
    // сумма должна быть равна 1.0
    double d{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 };
 
    if (d == 1.0)
        std::cout << "equal";
    else
        std::cout << "not equal";
}

Эта программа напечатает:

not equal

Чем больше вы выполняете арифметических действий с числом с плавающей запятой, тем больше в нем накапливаются небольшие ошибки округления.

Целочисленное деление

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

#include <iostream>
 
int main()
{
    int x{ 5 };
    int y{ 3 };
 
    std::cout << x << " divided by " << y << " is: " << x / y; // целочисленное деление
 
    return 0;
}

Этот код напечатает:

5 divided by 3 is: 1

В уроке «5.2 – Арифметические операторы» мы показали, что мы можем использовать static_cast для преобразования одного из целочисленных операндов в значение с плавающей запятой, чтобы выполнять деление с плавающей запятой.

Случайные пустые инструкции

В уроке «7.3 – Распространенные проблемы при работе с операторами if» мы рассмотрели пустые инструкции, которые ничего не делают.

В приведенной ниже программе мы хотим взорвать мир, только если у нас есть разрешение пользователя:

#include <iostream>
 
void blowUpWorld()
{
    std::cout << "Kaboom!n";
} 
 
int main()
{
    std::cout << "Should we blow up the world again? (y/n): ";
    char c{};
    std::cin >> c;
 
    if (c=='y'); // здесь случайная пустая инструкция
        blowUpWorld(); // поэтому это всегда будет выполняться, так как это не часть оператора if
 
    return 0;
}

Однако из-за случайной пустой инструкции вызов функции blowUpWorld() выполняется всегда, поэтому мы взрываем независимо от ввода:

Should we blow up the world again? (y/n): n
Kaboom!

Неиспользование составной инструкции, когда она требуется

Еще один вариант приведенной выше программы, которая всегда взрывает мир:

#include <iostream>
 
void blowUpWorld()
{
    std::cout << "Kaboom!n";
} 
 
int main()
{
    std::cout << "Should we blow up the world again? (y/n): ";
    char c{};
    std::cin >> c;
 
    if (c=='y')
        std::cout << "Okay, here we go...n";
        blowUpWorld(); // упс, всегда будет выполняться. Должно быть внутри составной инструкции.
 
    return 0;
}

Эта программа печатает:

Should we blow up the world again? (y/n): n
Kaboom!

Висячий else (рассмотренный в уроке «7.3 – Распространенные проблемы при работе с операторами if») также попадает в эту категорию.

Что еще?

Приведенные выше примеры представляют собой хороший образец наиболее распространенных типов семантических ошибок, которые склонны совершать на C++ начинающие программисты, но их гораздо больше. Читатели, если у вас есть дополнительные примеры, которые, по вашему мнению, являются распространенными ошибками, напишите об этом в комментариях.

Теги

C++ / CppLearnCppДля начинающихОбучениеПрограммирование


Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Эта статья посвящена перегрузке операторов. Особое внимание уделено использованию перегруженных операторов в стандартной библиотеке. Это вторая статья из серии, первая, посвященная перегрузке функций и шаблонов, находится здесь. Следующая статья будет посвящена перегрузке операторов управления памятью.


Оглавление

Введение

Перегрузка операторов (operator overloading) — это возможность применять встроенные операторы языка к разным типам, в том числе и пользовательским. На самом деле, это достаточно старая идея. Уже в первых языках программирования символы арифметических операций: +, -, etc. использовались для операций над целыми и вещественными числами, несмотря на то, что они имеют разный размер и разное внутреннее представление и, соответственно, эти операции реализованы по разному. С появлением объектно-ориентированных языков эта идея получила дальнейшее развитие. Если операции над пользовательскими типами имеют сходную семантику с операциями над встроенными типами, то почему бы не использовать синтаксис встроенных операторов. Это может повысить читаемость кода, сделать его более лаконичным и выразительным, упростить написание обобщенного кода. В C++ перегрузка операторов имеет серьезную поддержку и активно используется в стандартной библиотеке.

1. Общие вопросы перегрузки операторов

1.1. Перегружаемые операторы

В C++17 стандарт разрешает перегружать следующие операторы: +, -, *, /, %, ^, &, |, ~, !, ,, =, <, >, <=, >=, ++, –-, <<, >>, ==, !=, &&, ||, +=, -=, /=, %=, ^=, &=, |=, *=, <<=, >>=, [], (), ->, ->*, new, new[], delete, delete[].
(Обратим внимание на то, что этот список не менялся с C++98.) Последние четыре оператора, связанные с распределением памяти, в данной статье не рассматриваются, эта довольно специальная тема будет рассмотрена в следующей статье. Остальные операторы можно разделить на унарные, бинарные и оператор (), который может иметь произвольное число параметров. Операторы +, -, *, &, ++, –- имеют два варианта (иногда семантически совершенно разных) — унарный и бинарный, так, что фактически перегружаемых операторов на 6 больше.

1.2. Общие правила при выборе перегружаемого оператора

При перегрузке операторов надо стараться, чтобы смысл перегруженного оператора был очевиден для пользователя. Хороший пример перегрузки в этом смысле — это использование операторов + и += для конкатенации экземпляров std::basic_string<>. Оригинальное решение используется в классе std::filesystem::path (C++17). В этом классе операторы / и /= перегружены для конкатенации элементов пути. Конечно к делению это никакого отношения не имеет, но зато этот символ оператора совпадает с традиционным разделителем элементов пути. Запоминается с первого раза.

Необходимо учитывать приоритет и ассоциативность операторов, они при перегрузке не меняются и должны соответствовать ожиданиям пользователя. Характерный пример — это использование оператора << для вывода данных в поток. К сожалению, приоритет этого оператора довольно высок, поэтому скобками приходится пользоваться чаще, чем хотелось бы. Например

std::сout<<c?x:y;

это

(std::сout<<c)?x:y;

а не

std::сout<<(c?x:y);

как надо.

Проблема усугубляется наличием неявного преобразования от std::сout к void*, из-за чего эти инструкции компилируются без ошибок и предупреждений. По-хорошему, приоритет оператора записи данных в поток должен быть очень низким, на уровне оператора присваивания. Например оператор += подошел бы по смыслу и приоритету, но, увы, он правоассоциативный, а для вывода в поток нужен левоассоциативный оператор.

1.3. Операторы, не рекомендуемые для перегрузки

Не рекомендуется перегружать следующие три бинарных оператора: , (запятая), &&, ||. Дело в том, что для них стандарт предусматривает порядок вычисления операндов (слева направо), а для последних двух еще и так называемую семантику быстрых вычислений (short-circuit evaluation), но для перегруженных операторов это уже не гарантируется или просто бессмысленно, что может оказаться весьма неприятной неожиданностью для программиста. (Семантика быстрых вычислений, называемая еще закорачиванием, заключается в том, для оператора && второй операнд не вычисляется, если первый равен false, а для оператора || второй операнд не вычисляется, если первый равен true.)

Также не рекомендуется перегружать унарный оператор & (взятие адреса). Тип с перегруженным оператором & опасно использовать с шаблонами, так как они могут использовать стандартную семантику этого оператора. Правда в С++11 появилась стандартная функция (точнее шаблон функции) std::addressof(), которая умеет получать адрес без оператора & и правильно написанные шаблоны должны использовать именно эту функцию вместо встроенного оператора.

1.4. Интерфейс и семантика перегруженных операторов

Стандарт регламентирует не все детали реализации перегруженных операторов. При реализации почти всегда можно произвольно выбирать тип возвращаемого значения, для бинарных операторов тип одного из параметров. Тем не менее, весьма желательно, чтобы перегруженные операторы максимально близко воспроизводили интерфейс и семантику соответствующих встроенных операторов. В этом случае поведение кода, использующего перегруженные операторы, было бы максимально похожим на поведение кода, использующего встроенные операторы. Например, оператор присваивания должен возвращать ссылку на левый операнд, которая может быть использована как правый операнд в другом присваивании. В этом случае становятся допустимыми привычные выражения типа a=b=c. Операторы сравнения должны возвращать bool и не изменять операнды. Унарные операторы +, -, ~ должны возвращать модифицированное значение и не изменять операнд. Если реализация оператора возвращает объект по значению, то его часто объявляют константным. Это предотвращает модификацию возвращаемого значения, что позволяет предотвратить ряд синтаксических странностей, которых нет при использовании встроенных операторов (подробнее см. [Sutter1]). Но если возвращаемый тип является перемещаемым, то его нельзя объявлять константным, так как это ломает всю семантику перемещения. Другие примеры будут рассмотрены далее.

1.5. Реализация перегрузки операторов

1.5.1. Два варианта реализации перегрузки операторов

Операторы можно перегружать в двух вариантах: как функцию-член и как свободную (не-член) функцию. Четыре оператора можно перегрузить только как функцию-член — это =, ->, [], (). Для перечислений операторы можно перегружать только как свободные функции.

Для того, чтобы перегрузить оператор как функцию-член необходимо объявить нестатическую функцию-член с именем operator@, где @ символ(ы) оператора. В случае перегрузки унарного оператора эта функция не должна иметь параметров, а в случае бинарного должна иметь ровно один параметр. В случае перегрузки оператора () эта функция может иметь произвольное число параметров.

Для того, чтобы перегрузить оператор как свободную (не-член) функцию, необходимо объявить функцию с именем operator@, где @ символ(ы) оператора. В случае перегрузки унарного оператора, эта функция должна иметь один параметр, а в случае бинарного должна иметь два параметра. В случае перегрузки бинарного оператора — по крайней мере один из двух параметров, а в случае унарного единственный параметр должен быть того же типа (или типа ссылки), что и тип, для которого реализуется перегрузка. Так же эта функция должна находится в том же пространстве имен, что и тип, для которого реализуется перегрузка. Вот пример:

namespace N
{
    class X
    {
    // ...
        X operator+() const;           // унарный плюс
        X operator+(const X& x) const; // бинарный плюс
        void operator()(int x, int y); // вызов функции
        char operator[](int i);        // индексатор
    };
    X operator-(const X& x);             // унарный минус
    X operator-(const X& x, const X& y); // бинарный минус
}

Среди операторов, которые можно перегружать двумя способами, унарные операторы и присваивающие версии бинарных операторов обычно перегружают как функцию-член, а оставшиеся бинарные операторы как свободные функции.

1.5.2. Две формы использования перегруженных операторов

Использовать перегруженный оператор можно в двух формах (нотациях): инфиксной и функциональной. Инфиксная форма как раз и есть привычный синтаксис использования операторов.

Вот пример для класса из предыдущего раздела (будем считать, что код находится вне пространства имен N):

N::X x, y;
// инфиксная форма
N::X z = x + y;
N::X v = x – y;
N::X w = +x;
N::X u = -x;
x(1,2);
char p = x[4];
// функциональная форма
N::X z = x.operator+(y);
N::X v = operator-(x, y);
N::X w = x.operator+();
N::X u = operator-(x);
x.operator()(1,2);
char p = x.operator[](4);

Инфиксная форма, конечно, используется чаще, собственно ради нее и реализована перегрузка операторов. В ряде случаев, например, в контейнерах или в алгоритмах используется только инфиксная нотация. Но иногда удобно (и даже необходимо) использовать и функциональную форму, например для устранения неоднозначности. (Типичный пример — это вызов соответствующего оператора базового класса.) Особенности функциональной формы для оператора -> будут рассмотрены далее, в разделе, посвященном этому оператору.

Обратим внимание на то, что при использовании перегруженных операторов работает поиск, зависимый от типа аргумента (argument depended lookup, ADL), без него это использование, особенно в инфиксной форме, было бы весьма неудобно в случае, когда класс, для которого перегружается оператор, находится в другом пространстве имен. Вполне возможно, что ADL и появился в основном для решения этой проблемы.

1.5.3. Одновременное использование двух вариантов реализации перегрузки

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

2. Дополнительные подробности реализации перегрузки операторов

2.1. Множественная перегрузка

Один и тот же оператор можно перегрузить несколько раз. Для унарных операторов может быть всего два варианта — с квалификатором const и без него (для функций-членов), или варианты с параметром типа константная ссылка или обычная ссылка (для свободных функций). Для бинарных операторов и оператора () количество перегрузок не ограничено.

Например для std::string бинарный оператор + перегружен несколько раз: в одной версии оба параметра имеет тип const std::string&, в других один из параметров имеет тип const char*.

В разделе 3.4.2 рассматривается множественная перегрузка оператора ().

Бинарные операторы и оператор () могут быть шаблонами, что по существу является множественной перегрузкой.

2.2. Особенности перегрузки операторов с использованием свободных функций

Рассмотрим несколько ситуаций, когда перегрузка операторов с использованием свободных функций предпочтительней или, вообще, безальтернативна.

2.2.1. Симметрия

Одна из причин по которой для бинарных операторов свободные функции могут оказаться предпочтительными — это симметрия. Часто желательно, чтобы если корректным выражением является x@y, то корректным выражением было бы и y@x для любых допустимых типов. Для свободных функций мы можем выбирать произвольный тип первого операнда, когда как в случае функции-члена мы этого лишены. В качестве примера можно привести оператор + для std::string, когда один из операндов имеет тип const char*.

2.2.2. Расширение интерфейса класса

Перегрузка бинарных операторов с использованием свободных функций позволяет расширять интерфейс класса без добавления новых функций-членов. (Напомним, что интерфейс класса включает не только функции-члены, но и свободные функции с параметрами тип которых определяется этим классом.) В качестве примера можно привести перегрузку операторов вставки и извлечения из потока. Если бы мы для перегрузки этих операторов использовали функции-члены, то нам бы пришлось для каждого нового типа, вставляемого в поток или извлекаемого из потока, добавлять в потоковые классы соответствующие функции-члены, что понятное дело невозможно. Подробнее про перегрузку операторов вставки и извлечения из потока см. раздел 3.8.

2.2.3. Неявные преобразования

Еще одна причина по которой перегрузка с использованием свободных функций может оказаться предпочтительней — это неявные преобразования. Если оператор перегружен для некоторого класса X с использованием свободных функций, то этот оператор автоматически становится перегруженным для любого класса, имеющего неявное преобразование к X. Это справедливо как для унарных, так и для бинарных операторов. Для операторов, перегруженных как функция-член, это не всегда так. Вот пример.

int i = 42;
std::reference_wrapper<int> rwi(i);
std::cout << rwi << 'n'; // вывод: 42

Операторы вставки и извлечения из потока не перегружены для std::reference_wrapper<int>, но этот класс имеет неявное преобразование к int&, поэтому приведенный код компилируется и выполняется. Правда проблемы могут возникнуть, если перегруженный оператор является шаблоном, так как при конкретизации шаблона функции неявные преобразования не используются. В этом случае может помочь прием с определением оператора как дружественной свободной функции внутри шаблона, рассмотренный в разделе 2.3.

2.2.4. Перечисления

Для перечислений операторы можно перегружать только как свободные функции, так как у перечислений просто не может быть функций-членов, пример см. в разделе 2.6.

2.3. Определение дружественной свободной функции внутри класса

Часто свободным функциям, реализующим оператор, целесообразно иметь доступ к закрытым членам класса и поэтому их объявляют дружественными. Напомним, что синтаксис дружественных функций позволяет разместить их определение непосредственно в теле класса.

class X
{
// ...
    friend X operator+(const X& x, const X& y) // бинарный плюс
    {
    // ...
    }
};

Такой стиль подчеркивает связь оператора с классом и позволяет сделать определение более лаконичным. В случае шаблонов этот прием не только делает определение более лаконичным, но и расширяет функциональность оператора, позволяет использовать неявные преобразования аргументов, которые недоступны при определении шаблона функции вне класса. Поэтому его можно использовать, даже когда не нужен доступ к закрытым членам. Рассмотрим пример, являющийся небольшой переработкой примера из [Meyers1]. В этом примере бинарный оператор + определен внутри класса с использованием ключевого слова friend, а бинарный оператор - определен вне класса.

// rational number (рациональное число)
template<typename T>
class Rational 
{
    T num; // numerator (числитель)
    T den; // denominator (знаменатель)
public:
    Rational(T n = 0, T d = 1) : num(n), den(d) {/* ... */}

    T Num() const { return num; }
    T Den() const { return den; }

    friend const Rational operator+(
        const Rational& x, const Rational& y)
    {
        return Rational(
                x.num * y.den + y.num * x.den,
                x.den * y.den);
    }
};
template<typename T>
const Rational<T>operator-(
    const Rational<T>& x, const Rational<T>& y)
{
    return Rational<T>(
        x.Num() * y.Den() - y.Num() * x.Den(), 
        x.Den() * y.Den());
}

Определение оператора + позволяет использовать закрытые члены класса. Но это еще не все, такое определение дает возможность при сложении использовать неявное преобразование от T к Rational, определенное в классе с помощью конструктора с одним параметром. Вот пример:

Rational<int> r1(1, 2), r2(31, 64);
Rational<int> r3 = r1 + r2; // Rational + Rational
Rational<int> r4 = r1 + 3;  // Rational + int
Rational<int> r5 = 4 + r2;  // int + Rational

В последних двух инструкциях мы складываем объекты типа Rational со значениями типа int. К аргументам типа int применяется неявное преобразование от int к Rational, инструкции компилируются и выполняются без ошибки.

Попробуем теперь использовать оператор -.

Rational<int> r6 = r1 - 3; // Rational - int
Rational<int> r7 = 4 - r2; // int - Rational

В обоих случаях компилятор выдает ошибку, неявное преобразование от int к Rational в данном случае не работает. Для того, что бы эти инструкции были корректными, надо добавить еще два варианта перегрузки оператора -:

template<typename T>
const Rational<T> operator-(const Rational<T>& x, T y)
{
    return operator-(x, Rational<T>(y));
}

template<typename T>
const Rational<T> operator-(T x, const Rational<T>& y)
{
    return operator-(Rational<T>(x), y);
}

Подробнее см. [Meyers1].

2.4. Вычислительные конструкторы

Если оператор возвращает объект по значению, иногда целесообразно определить специальный закрытый конструктор, называемый вычислительным конструктором (computational constructor). В этом случае компилятор сможет применить оптимизацию возвращаемого значения (return value optimization, RVO). Подробнее см. [Dewhurst].

2.5. Виртуальные операторы

Если оператор перегружен как функция-член, его можно объявить виртуальным. Реализация оператора, перегруженного как свободная функция, может использовать виртуальные функции параметров (своего рода идиома NVI – non virtual interface). Но «философия» перегрузки операторов плохо согласуется с полиморфизмом. Полиморфные объекты обычно доступны через указатели. В инфиксной форме вызов оператора можно сделать только через ссылку, поэтому приходится использовать не очень изящные выражения, например *a+*b. При реализации некоторых бинарных операторов, перегруженных как свободная функция (например +), приходится реализовывать двойную диспетчеризацию, а это не очень просто (паттерн Visitor). Оператор присваивания не рекомендуется делать виртуальным, про это написано довольно много, см. например [Dewhurst]. Присваивание является неполиморфной по своей сути операцией. В общем, можно сказать, что виртуальные перегруженные операторы — это не самая лучшая идея.

2.6. Перегрузка операторов для перечислений

Операторы, перегружаемые как свободная функция, можно перегрузить для перечислений. Вот пример:

enum class Color { Begin, Red = Begin, Green, Blue, End};
// перегрузка инкремента
Color& operator++(Color& col) { return (Color&)(++(int&)col); }

Теперь перебрать все элементы перечисления можно так:

void Foo(Color col);
// ...
for (Color col = Color::Begin; col < Color::End; ++col)
{
    Foo(col);
}

Перегрузим еще один оператор

Color operator*(Color col) { return col; }

Теперь перебрать все элементы перечисления можно с помощью стандартного алгоритма:

std::for_each(Color::Begin, Color::End, Foo);

И еще один вариант. Определим класс:

struct Colors
{
    Color begin() const { return Color::Begin; }
    Color end() const { return Color::End; }
};

После этого перебрать все элементы перечисления можно с помощью диапазонного for:

for (auto col : Colors())
{
    Foo(col);
}

3. Особенности перегрузки некоторых операторов

В этом разделе описываются особенности перегрузки некоторых операторов, особое внимание уделяется использованию этих перегрузок в стандартной библиотеке.

3.1. Оператор ->

Этот оператор является унарным и может быть реализован только как функция-член (обычно константная). Он должен возвращать либо указатель на класс (структуру, объединение), либо тип, для которого перегружен оператор ->. Перегрузка этого оператора используется для «указателеподобных» типов — интеллектуальных указателей и итераторов. Вот пример:

class X
{
// ...
    void Foo();
};
class XPtr
{
// ...
    X* operator->() const;
};
// ...
X x;
x->Foo();              // инфиксная форма
x.operator->()->Foo(); // функциональная форма

В стандартной библиотеке оператор -> перегружен для интеллектуальных указателей и итераторов.

3.2. Унарный оператор *

Этот унарный оператор часто перегружают в паре с оператором ->. Как правило, он возвращает ссылку на элемент, указатель на который возвращает оператор ->. Этот оператор обычно реализуется как константная функция-член.

В стандартной библиотеке оператор * перегружен для интеллектуальных указателей и итераторов.

3.3. Оператор []

Этот бинарный оператор, который обычно называют индексатором, может быть реализован только, как функция-член, которая должна иметь ровно один параметр. Тип этого параметра произвольный, соответственно, перегрузок может быть несколько, для разных типов параметра. Индексатор обычно перегружается для «массивоподобных» типов, а также для других контейнеров, например ассоциативных массивов. Возвращаемое значение обычно является ссылкой на элемент контейнера. Также, в принципе, может быть возврат по значению, но следует иметь в виду, что при этом для получения адреса элемента нельзя будет использовать выражения &х[i], допустимые для встроенного индексатора. Такое выражение не будет компилироваться, если возвращаемый тип встроенный, и будет давать адрес временного объекта для пользовательского возвращаемого типа.

Индексатор часто перегружают в двух вариантах — константном и неконстантном.

T& operator[](int ind);
const T& operator[](int ind) const;

Первая версия позволяет модифицировать элемент, вторая только прочитать и она будет выбрана для константных экземпляров и в константных функциях-членах.

В стандартной библиотеке индексатор перегружен для последовательных контейнеров std::vector<>, std::array<>, std::basic_string<>, std::deque<> и ассоциативных контейнеров std::map<>, std::unordered_map<>. Специализация для массивов интеллектуального указателя std::unique_ptr<> также перегружает индексатор.

3.3.1. Многомерные массивы

C++ поддерживает только одномерные массивы, то есть выражение a[i,j] некорректно, но многомерность моделируется в виде «массива массивов», то есть можно использовать выражение a[i][j]. Этот синтаксис несложно поддержать для пользовательских индексаторов с помощью промежуточного прокси-класса. Вот пример простого шаблона матрицы.

template<typename T>
class Matrix
{
public:
    Matrix(int rowCount, int colCount);

    class RowProxy;
    RowProxy operator[](int i) const;

    class RowProxy
    {
    public:
        T& operator[](int j);
        const T& operator[](int j) const;
        // ...
    };
    // ...
};
// ...
Matrix<double> mtx(5, 6);
double s = mtx[1][2];
mtx[2][3] = 3.14;

3.4. Оператор ()

Этот оператор можно реализовать только как функцию-член. Он может иметь любое число параметров любого типа, тип возвращаемого значения также произвольный. Классы, с перегруженным оператором (), называются функциональными, их экземпляры называются функциональными объектами или функторами. Функциональные классы и объекты играют очень важную роль в программировании на C++ и в частности активно используются в стандартной библиотеке. Именно с помощью таких классов и объектов в C++ реализуется парадигма функционального программирования. Функциональные классы и объекты, используемые в стандартной библиотеке, в зависимости от назначения имеют свои названия: предикаты, компараторы, хеш-функции, аккумуляторы, удалители. В зависимости от контекста использования, стандартная библиотека предъявляет определенные требования к функциональным классам. Экземпляры этих классов должны быть копируемыми по значению, не модифицировать аргументы, не иметь побочных эффектов и изменяемое состояние (чистые функции), соответственно реализация перегрузки оператора () обычно является константной функцией-членом. Есть исключение — алгоритм std::for_each(), для него функциональный объект может модифицировать аргумент и иметь изменяемое состояние.

3.4.1. Локальные определения и лямбда-выражения

В C++ нельзя определить функцию локально (в блоке). Но можно определить локальный класс и этот класс может быть функциональным. Столь популярные в народе лямбда-выражения как раз и представляют из себя средство для быстрого и удобного определения анонимного локального функционального класса на «на лету».

3.4.2. Мультифункциональные типы и объекты

Функциональный класс может иметь несколько вариантов перегрузки оператора (), с разными параметрами. Такие классы и соответствующие объекты можно назвать мультифункциональными. Пример использования мультифункциональных объектов в стандартной библиотеке приведен в Приложении А.

3.4.3. Хеш-функция

Неупорядоченные контейнеры ( std::unordered_set<>, std::unordered_multiset<>, std::unordered_map<>, std::unordered_multimap<>) требуют для своей работы функциональные объекты, которые реализуют вычисление хеш-функции для элементов контейнера или ключей. Такие контейнеры предусматривают шаблонный параметр функционального типа для реализации вычисления хеш-функции. Для этого типа перегруженный оператор () должен принимать ссылку на элемент или ключ и возвращать хеш-значение типа std::size_t. Если пользователь не задал необходимый функциональный тип, контейнер предоставляет необходимый тип по умолчанию. Для этого используются шаблон класса std::hash<>, которые конкретизируются для типа элементов контейнера или ключа. Этот шаблон специализирован для числовых типов, указателей и некоторых стандартных типов. Для типов, не имеющих специализации, программист должен самостоятельно реализовать хеш-функцию. Это можно сделать двумя способами.

  1. Определить полную специализацию этого шаблона.
  2. Определить нужный функциональный класс и передать его в качестве шаблонного аргумента при конкретизации шаблона контейнера.

В Приложении Б приводится пример решения для C-строк на основе полной специализации стандартного шаблона.

3.4.4. Сравнение элементов и ключей в контейнерах

Ассоциативные и неупорядоченные контейнеры требуют для своей работы функциональные объекты, которые реализуют необходимые операции сравнения для элементов контейнера или ключей. Такие контейнеры предусматривают шаблонный параметр функционального типа для реализации необходимых операций. Для этого типа перегруженный оператор () должен иметь два параметра, ссылки на элементы или ключи, и возвращать bool. Если пользователь не задал необходимый функциональный тип, контейнер предоставляет необходимый тип по умолчанию. Для этого используются шаблоны std::less<> и std::equal_to<>, которые конкретизируются для типа элементов контейнера. Первый из них для реализации необходимой функциональности использует встроенный или перегруженный оператор <, второй встроенный или перегруженный оператор ==.

Шаблон std::less<> используется для сравнения по умолчанию элементов или ключей в ассоциативных контейнерах std::set<>, std::multiset<>, std::map<>, std::multimap<>, а также в контейнере std::priority_queue<>.

Шаблон std::equal_to<> используется для сравнения по умолчанию элементов или ключей в неупорядоченных контейнерах std::unordered_set<>, std::unordered_multiset<>, std::unordered_map<>, std::unordered_multimap<>.

Если для использования некоторого типа в контейнере стандартной библиотеки требуется изменить или определить сравнение элементов этого типа, то существует три способа решить эту проблему.

  1. Перегрузить оператор сравнения, который будет использован по умолчанию. Этот способ не подходит для встроенных типов, если нужно сравнение, отличное от стандартного, и для пользовательских типов, у которых нужный оператор уже перегружен (но не так как надо).
  2. Определить полную специализацию стандартных функциональных шаблонов, используемых для сравнения по умолчанию.
  3. Определить нужный функциональный класс и передать его в качестве шаблонного аргумента при конкретизации шаблона контейнера.

В Приложении Б приводится пример решения для C-строк на основе полной специализации стандартного шаблона.

3.4.5. Удалители в интеллектуальных указателях

Деструктор интеллектуального указателя должен освободить объект, которым владеет. Для этого используется функциональный объект, называемый удалителем (deleter). Соответствующий функциональный тип перегружает оператор () который должен принимать указатель на объект и не возвращать значение. Для std::unique_ptr<> по умолчанию используется шаблон std::default_delete<>, который конкретизируются для типа управляемого объекта и для его удаления использует оператор delete (или delete[] в случае специализации для массивов). Для std::shared_ptr<> по умолчанию используется оператор delete. Если необходима иная операция освобождения объекта, то необходимо определить свой функциональный тип. Это можно сделать двумя способами.

  1. Для std::unique_ptr<> определить полную специализацию стандартного шаблона-удалителя.
  2. Определить функциональный класс и использовать его или его экземпляры в качестве аргумента при создании интеллектуального указателя в соответствии с синтаксисом инициализации используемого интеллектуального указателя.

Полную специализацию стандартного шаблона-удалителя можно также использовать и для std::shared_ptr<>, для этого экземпляр этого удалителя надо передать вторым аргументом в конструктор std::shared_ptr<>.

3.4.6. Алгоритмы

Алгоритмы стандартной библиотеки активно используют функциональные объекты и, соответственно, многие из них имеют параметр функционального типа. Часто алгоритмы имеют версию без такого параметра, в этом случае для реализации необходимых операций используется оператор (встроенный или перегруженный), определенный для элементов диапазона.

Если не задан необходимый функциональный объект, то оператор < используется по умолчанию в алгоритме std::lexicographical_compare(), который сравнивает диапазоны, в алгоритмах поиска минимума/максимума (min_element(), etc), в алгоритмах, связанных с сортировкой и отсортированными данными (std::sort(), etc), в алгоритмах, связанных с пирамидой (std::make_heap(), etc).

Оператор == используется по умолчанию в алгоритме std::equal(), который сравнивает диапазоны, в алгоритме std::count(), который подсчитывает количество заданных элементов, в алгоритмах поиска (std::find(), etc), в алгоритмах std::replace() и std::remove(), которые модифицируют диапазон.

Оператор + используется по умолчанию в алгоритме accumulate(). (Подробнее см. Приложение А.)

Если для использования некоторого типа в алгоритме стандартной библиотеки требуется изменить или определить необходимые операции для элементов этого типа, то существует два способа решить эту проблему.

  1. Перегрузить оператор, который будет использован по умолчанию. Такой способ не всегда подходит, это обсуждалось выше.
  2. Определить нужный функциональный класс или функцию, и использовать их как аргумент алгоритма. (В этом случае часто используют лямбда-выражение.)

Пример для алгоритма сортировки C-строк приведен в Приложение Б.

3.4.7. Функциональный шаблон

В C++11 появился универсальный функциональный шаблон. Он конкретизируется типом функции и перегружает оператор () в соответствии с сигнатурой функции. Экземпляры конкретизации можно инициализировать указателем на функцию, функциональным объектом или лямбда-выражением с соответствующей сигнатурой. Вот пример.

#include <functional>

int Foo(const char* s) { return *s; }

struct X
{
    int operator() const (const char* s) { return *s; }
};

std::function<int(const char*)>
    f1 = Foo,
    f2 = X(),
    f3 = [](const char* s) { return *s; };

int r1 = f1("1"),
    r2 = f2("2"),
    r3 = f3("3");

3.5. Операторы сравнения

Операторы сравнения перегружают как свободные функции с двумя аргументами. Перегруженный оператор не должен изменять операнды и должен возвращать bool.

Чаще всего пользовательские типы перегружают операторы < и ==, для того чтобы элементы этого типа можно было хранить в контейнерах и использовать в алгоритмах. Об этом достаточно много говорилось в предыдущем разделе. Но для корректной работы контейнеров и алгоритмов операторы должны удовлетворять определенным критериям (см. [Josuttis]). Для оператора < это следующие свойства: антисимметричность (если x<y равно true, то y<x равно false), транзитивность (если x<y и y<z, то x<z), иррефлексивность (x<x всегда равно false), транзитивная эквивалентность (если !(x<y) && !(y<x) и !(y<z) && !(z<y), то !(x<z) && !(z<x)). Для оператора == это следующие свойства: симметричность (если x==y, то y==x), транзитивность ( если x==y и y==z, то x==z), рефлексивность (x==x всегда равно true). Естественно, что встроенные операторы отвечают этим критериям. Если для контейнеров и алгоритмов используются пользовательские функциональный типы, то они должны отвечать этим же критериям.

Рассмотрим теперь перегрузку остальных операторов сравнения. Встроенные операторы сравнения являются сильно зависимыми. Базовыми являются операторы < и ==, остальные можно выразить через них переставляя операнды и используя встроенный оператор !. Естественно, что при перегрузке операторов сравнения надо поступать таким же образом. Специально для этого в пространстве имен std::rel_ops таким способом определены операторы <=, >, >=, !=. (Заголовочный файл <utility>.) Вот пример использования этих операторов.

#include <utility>

class X { /* ... */ };
// базовые операторы
bool operator==(const X& lh, const X& rh);
bool operator<(const X& lh, const X& rh);
// зависимые операторы
bool operator<=(const X& lh, const X& rh)
{
    return std::rel_ops::operator<=(lh, rh);
}
// остальные зависимые операторы

Перегружать зависимые операторы для класса не обязательно, можно использовать операторы из std::rel_ops непосредственно, для этого надо воспользоваться using-директивой:

using namespace std::rel_ops;

В стандартной библиотеке полный набор операторов сравнения — <, <=, >, >=, ==, !=, перегружают контейнеры и интеллектуальные указатели, а также некоторые более специальные классы: std::thread::id, std::type_index, std::monostate. Контейнеры для реализации этих перегрузок используют соответствующие операторы для элементов, если элементы не поддерживают операцию, возникает ошибка. Интеллектуальные указатели используют соответствующие встроенные операторы для указателей.

Операторы == и != перегружают std::error_code, std::bitset. Также эти операторы является частью стандартного интерфейса любого итератора, а оператор < является частью стандартного интерфейса итераторов произвольного доступа.

3.6. Арифметические операторы

Бинарные операторы арифметических операций обычно перегружают в паре с соответствующим присваивающим оператором, например + и +=. Первый перегружают как свободную функцию с двумя аргументами, второй — как функцию-член. Унарные операторы, +, -, перегружают как функцию-член. Вот пример.

class X
{
// ...
    const X operator-() const; // унарный минус
    X& operator+=(const X& x); // присваивающий плюс
};
const X operator+(const X& x, const X& y); // бинарный плюс

Унарные операторы +, - не должны изменять операнд (в отличии от инкремента и декремента) и должны возвращать результат по значению. Такую семантику имеют встроенные версии этих операторов. Свободная функция, реализующая бинарный оператор, также не изменяет операндов и возвращает результат по значению. Присваивающая версия реализована как функция-член, которая не изменяет второй операнд и возвращает *this. В данном примере возвращаемый тип для операторов + и += объявлен константным, причина описана в разделе 1.4.1, но это надо делать только, если тип X не поддерживает семантику перемещения, в противном случае const надо убрать.

В бинарных операторах тип операндов может не совпадать. Например для строк один из операндов может быть C-строкой, для итераторов произвольного доступа второй операнд является сдвигом. Но в таком случае надо подумать о симметрии (см. раздел 2.2).

В стандартной библиотеке полный набор арифметических операторов перегружает std::complex<>. Операторы +, +=, -, -= перегружают итераторы произвольного доступа. В std::basic_string<> операторы + и += перегружаются для реализации конкатенации. Оригинальное решение используется в классе std::filesystem::path (C++17). В этом классе операторы / и /= перегружены для конкатенации элементов пути. Конечно к делению это никакого отношения не имеет, но зато этот символ оператора совпадает с традиционном разделителем элементов пути. Запоминается с первого раза.

3.7. Инкремент, декремент

Эти операторы являются частью стандартного интерфейса итератора. Префиксные формы являются унарными операторами, постфиксные бинарными с фиктивным вторым параметром целого типа. Обе они обычно реализуются как функции-члены и постфиксный вариант определяется через префиксный. Вот типичная реализация инкремента.

class Iter
{
public:
    Iter& operator++() // префиксный инкремент
    {
        // реализация инкремента
        return *this;
    }

    const Iter operator++(int) // постфиксный инкремент
    {
        Iter it(*this);
        ++*this;
        return it;
    }
    // ...
};

Итераторы являются копируемыми типами без поддержки перемещения, поэтому постфиксный инкремент должен возвращать константный объект, это предотвращает модификацию возвращаемого значения, см. раздел 1.4.

В стандартной библиотеке инкремент перегружают все итераторы, а декремент двунаправленные итераторы и итераторы произвольного доступа.

3.8. Операторы << и >>

Перегрузка этих операторов используется в стандартной библиотеке для вставки объектов в текстовой поток и извлечения объектов из текстового потока (поэтому в этом качестве их еще называют оператором вставки в поток и оператором извлечения из потока). Перегружаются они всегда как свободные функции, их сигнатура подчиняется правилам: первый операнд является ссылкой на поток, второй операнд является ссылкой на вставляемый или извлекаемый объект, возвращаемое значение является ссылкой на поток. Вот пример.

#include <iostream>

struct Point
{
    int X;
    int Y;
};
std::ostream& operator<<(std::ostream& strm, const Point& p)
{
    strm << '[' << p.X << ',' << p.Y << ']';
    return strm;
}

Главная проблема этих перегрузок — довольно высокий приоритет операторов, поэтому скобками приходится пользоваться чаще, чем хотелось бы. Проблема усугубляется наличием неявного преобразования потоковых типов к void*, из-за чего компилятор может не выдавать ошибок. Примеры см. в разделе 1.2.

3.9. Оператор присваивания

Оператор присваивания можно реализовать только, как функцию-член, которая должна иметь ровно один параметр. Тип этого параметра произвольный, соответственно, перегрузок может быть несколько, для разных типов параметра. Перегрузка оператора присваивания является составной частью поддержки семантики копирования/перемещения и к ней приходится прибегать достаточно часто. Оператор присваивания практически всегда идет в паре с конструктором, имеющим один параметр. Нормальная ситуация — это когда каждому конструктору с одним параметром прилагается соответствующий оператор присваивания. Если описать семантику присваивания «на пальцах», то присваивание должно полностью освободить все текущие ресурсы, которыми владеет объект (левый операнд), и на его месте создать новый объект, определяемый правым операндом.

Среди операторов присваивания выделяются два стандартных — оператор копирующего присваивания и оператор перемещающего присваивания, которые соответствуют копирующему конструктору и перемещающему конструктору.


class X
{
public:
    X(const X& src);     // копирующий конструктор
    X(X&& src) noexcept; // перемещающий конструктор

    X& operator=X(const X& src);     // оператор копирующего присваивания
    X& operator=X(X&& src) noexcept; // оператор перемещающего присваивания
// ...
};

Обратим внимание на то, что для перемещающих операций крайне желательно гарантировать, чтобы они не выбрасывали исключений. Такие функции-члены надо объявлять как noexcept.

Стандартные операторы присваивания могут быт сгенерированы компилятором. Для этого при объявлении надо использовать конструкцию "=default".

class X
{
public:
    X& operator=X(const X& src) = default;
    X& operator=X(X&& src) = default; 
// ...
};

Компилятор может сгенерировать стандартные операторы присваивания и без такой подсказки. Если это не желательно, то можно явно запретить такую генерацию, объявив эти операторы удаленными.

class X
{
public:
    X& operator=X(const X& src) = delete;
    X& operator=X(X&& src) = delete; 
// ...
};

Рассмотрим теперь вопрос реализации операторов присваивания. Оператор присваивания обычно возвращает ссылку на текущий объект, то есть *this. Это нужно для того, чтобы для стандартных операторов присваивания были допустимыми выражения типа a=b=c. Наиболее прогрессивный вариант реализации операторов присваивания — это использование идиомы «копирование и обмен». Для этого в классе должна быть определена функция-член обмена состояниями, которая не должна выбрасывать исключений.

class X
{
public:
    void Swap(X& src) noexcept;     // обмен состояниями
    X(const X& src);                // копирующий конструктор
    X(X&& src) noexcept;            // перемещающий конструктор
    X& operator=X(const X& src);
    X& operator=X(X&& src) noexcept;
// ...
};

И тогда операторы присваивания реализуются с помощью соответствующего конструктора и функции обмена состояниями следующим образом:

X& X::operator=X(const X& src)
{
    X tmp(src); // копирование
    Swap(tmp);
    return *this;
}
X& X::operator=X(X&& src) noexcept
{
    X tmp(std::move(src)); // перемещение
    Swap(tmp);
    return *this;
}

Аналогично можно определить оператор присваивания, соответствующий любому другому конструктору с одним параметром.
Главное достоинства этой идиомы состоит в обеспечении строгой гарантии безопасности исключений: если в конструкторе произошло исключение, то объект останется в том же состоянии, что и до начала операции (транзакционная семантика).

Если идиома «копирование и обмен» не используется, то необходима проверка на самоприсваивание.

X& X::operator=X(const X& src)
{
    if (this != std::addressof(src))
    {
        // ...
    }
    return *this;
}

Также, в случае наследования, надо вызвать соответствующий оператор базового класса. Еще одно достоинство идиомы «копирование и обмен» как раз и состоит в том, что она корректно работает при самоприсваивании, хотя, конечно, и не оптимально.

Ну и, наконец, рассмотрим довольно известную антиидиому для реализации присваивания.

X& X::operator=X(const X& src)
{
    if (this != std::addressof(src))
    {
        this->~X();
        new (this)(src);
    }
    return *this;
}

В этом случае сначала явно вызывается деструктор для this, потом с помощью размещающего new на месте, куда указывает this, создается новый объект. На первый взгляд этот код соответствует описанию семантики оператора, приведенной в начале раздела, но если разобраться, то он имеет существенные дефекты. Если конструктор выбрасывает исключение, то место в памяти, на которое указывает this, превращается в кусок памяти, содержимое которого не определено. Любая попытка использовать объект закончится неопределенным поведением. Другая проблема возникает, когда X является базовым классом для какого-нибудь другого класса и деструктор класса X виртуальный. В этом случае this->~X() уничтожает объект производного класса, что может полностью сломать взаимодействие базового класса и производного. Никогда так не делайте.

Оператор копирующего присваивания и оператор перемещающего присваивания (вместе с соответствующим конструктором) приходится перегружать практически всегда, когда нужна нестандартная семантика копирования/перемещения. (Запрет копирующего или перемещающего присваивания также можно рассматривать как перегрузку.) Также оператор присваивания обычно перегружается, как парный для конструктора с одним параметром. Практически все классы стандартной библиотеки перегружают операторы присваивания.

3.10. Оператор !

Этот унарный оператор иногда перегружают для того чтобы проверять, не является ли объект не инициализированным. Он должен возвращать true, если объект не инициализирован («пустой», «нулевой»). В настоящее время такое решение не очень популярно. Сейчас чаще используют explicit преобразование к bool, с противоположной семантикой, оно должно возвращать true, если объект инициализирован.

explicit operator bool() const noexcept;

4. Итоги

Тщательно продумывайте перегрузку операторов. Она должна повысить наглядность и читаемость кода, но не наоборот.

При реализации перегрузки оператора учитывайте интерфейс и семантику встроенного оператора.

Приложения

Приложение А. Пример использования мультифункциональных объектов

Первый пример относится к шаблону std::variant<> (C++17). Шаблон функции std::visit() в качестве первого параметра используют мультифункциональный класс, у которого оператор () перегружен для всех типов конкретизации std::variant<>, а второй параметр является этой самой конкретизацией. Вызов std::visit() обеспечивает вызов версии оператора () мультифункционального объекта, соответствующей фактическому типу std::variant<>. Вот пример.

using IntStr = std::variant<int, std::string>;

struct Visitor
{
    void operator()(int x) const
    { 
        std::cout << "int, val=" << x << 'n';
    }
    void operator()(const std::string& x) const
    {
        std::cout << "string, val=" << x << 'n';
    }
};
// ...
IntStr a(42), b("meow");
Visitor v;
std::visit(v, a); // вывод: int, val=42
std::visit(v, b); // вывод: string, val=meow

Другой пример относится к алгоритму std::reduce() (C++17). Этот алгоритм является параллельной версией алгоритма std::accumulate(). Рассмотрим сначала старый std::accumulate().

template<class InputIt, class T, class BinOper>
T accumulate(InputIt first, InputIt last, T init, BinOper oper);

BinOper — это функциональный тип, совместимой с сигнатурой

T f(T t, S s);

Первый параметр имеет тип T — аккумулирующий тип, второй параметр имеет тип S — тип элементов последовательности, возвращаемое значение имеет типа T. В простейших случаях, таких как сумма, T и S могут совпадать, но в общем случае это не так. Алгоритм последовательно вызывает функциональный объект для всех элементов последовательности, передавая их как второй аргумент, а в качестве первого аргумента использует результат вызова на предыдущим шаге. На первом шаге используется init. Это исключительно последовательный алгоритм, поэтому в C++17 добавили алгоритм std::reduce(), решающий ту же задачу, но с поддержкой распараллеливания.

template<class ExecutionPolicy,
    class InputIt, class T, class BinOper>
T reduce(ExecutionPolicy&& policy,
    InputIt first, InputIt last, T init, BinOper oper);

Ключевое отличие BinOper от аналогичного в std::accumulate() — это то, что BinOper должен поддерживать несколько сигнатур:

T f(T t, S s);
T f(S s1, S s2);
T f(T t1, T t2);

Приложение Б. Хэш-функция и сравнение для C-строк

C-строки — строки с завершающим нулем, — обычно представляются типом T* или const T*, где T один из символьных типов (char, wchar_t, etc). Но соответствующие конкретизации std::hash<>, std::less<> и std::equal_to<> будут рассматривать этот тип как указатель, игнорируя содержимое строки, что в большинстве случаев неприемлемо. Вот пример возможного решения.

#include <functional>

template <class T>
inline void hash_combine(std::size_t& seed, const T& v)
{
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + 
        (seed << 6) + (seed >> 2);
}

#include <cstring>

namespace std
{
    template <>
    struct hash<const char*>
    {
        size_t operator()(const char* str) const
        {
            std::size_t hash = 0;
            for (; *str; ++str)
            {
                hash_combine(hash, *str);
            }
            return hash;
        }
    };

    template <>
    struct equal_to<const char*>
    {
        bool operator()(const char* x, const char* y) const
        {
            return strcmp(x, y) == 0;
        }
    };

    template <>
    struct less<const char*>
    {
        bool operator()(const char* x, const char* y) const
        {
            return strcmp(x, y) < 0;
        }
    };
} // namespace std

Функция hash_combine() — это хорошо известная функция из библиотеки Boost. Она может быть использована при создании других пользовательских хеш-функций.

Ну и, наконец, пример сортировки C-строк в котором используется лямбда-выражение для определения нужного функционального объекта.

#include <cstring>
const char* cc[] = { "one", "two", "three", "four" };

std::sort(cc, cc + _countof(cc),
    [](const char* x, const char* y)
        { return std::strcmp(x, y) < 0; });

Список литературы

[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.
[Dewhurst]
Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.
[Meyers1]
Мэйерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.
[Sutter1]
Саттер, Герб. Решение сложных задач на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.

Понравилась статья? Поделить с друзьями:
  • C вывод ошибки try catch
  • C windows system32 taskmgr exe ошибка файловой системы 1073741819
  • C windows system32 d3d11 dll ошибка
  • C windows minidump ошибка как исправить
  • C windows memory dmp ошибка