Вы находитесь на странице: 1из 25

СТАНДАРТ С++

Часть третья: перегрузка

К. Владимиров, Intel, 2020


mail-to: konstantin.vladimirov@gmail.com
Вопрос для новичка
• Имеет ли смысл explicit для конструктора от двух аргументов?
• А что насчёт конструктора без аргументов? Обоснуйте ответ
• Предположим, что в обоих случаях нет параметров по умолчанию
struct S { S(int) {} };
struct T { explicit T(int) {} };
void foo(S);
void bar(T);
int main() {
foo(42); // works
bar(42); // fails
2 https://godbolt.org/z/da3d7v
Ответ и новый вопрос
• Ответ: да, имеет, у нас же есть инициализация из фигурных скобок
• [over.match.list] ... candidate functions are all the constructors of the class T and
the argument list consists of the elements of the initializer list ...
• struct S { S(int, int) {} };
struct T { explicit T(int, int) {} };
void foo(S);
void bar(T);
int main() {
foo({42, 42}); // works
bar({42, 42}); // fails
• Вопрос чуть сложнее: а есть ли смысл такое блокировать?
3 https://godbolt.org/z/Yxxsqf
Странности перегрузки
• Если у нас есть незаблокированный конструктор, он рассматривается при
разрешении перегрузки
void foo(S0) { std::cout << "S0" << std::endl; }
void foo(T0) { std::cout << "T0" << std::endl; }
• И это ошибка разрешения перегрузки
foo({}); // (1), failed
• Ещё один вопрос: можно ли сделать строчку (1) законной, не удалив ни одну
из перегрузок выше, а добавив ещё одну?

4
Разрешение перегрузки
• Ответ удивительно прост: добавить перегрузку, которая выиграет
void foo(int) { std::cout << "int" << std::endl; }
void foo(S0) { std::cout << "S0" << std::endl; }
void foo(T0) { std::cout << "T0" << std::endl; }
• И это ошибка разрешения перегрузки
foo({}); // (1), ok
• Теперь (1) будет разрешено в value-инициализацию целого числа

5 https://godbolt.org/z/3WWE49
Что мы знаем о перегрузке?
• Обычно я учу перегрузке так:
1. Точное совпадение выигрывает у всего
• Совпадение с правильной ссылкой это точное совпадение
• Совпадение с шаблоном проигрывает точному совпадению и таким образом не
является им (так как требует инстанцирования)
• Есть несколько уровней совпадений с более или менее общими шаблонами

2. Стандартные преобразования выигрывают у пользовательских


• Пользовательское преобразование это или конструктор или оператор

3. Троеточия всему проигрывают и это полезно для SFINAE


• Но сегодня я воспользуюсь случаем рассказать о перегрузке правду
6
Постановка проблемы
struct S { explicit S(int, int, int) {} };
struct T { T(int, int, int) {} };
void foo(struct S) {}
void foo(struct T) {}
int main() {
S s{1, 2, 3};
foo({1, 2, 3}); // error or ok?
}

7
Простая интуиция
struct S { explicit S(int, int, int) {} };
struct T { T(int, int, int) {} };
void foo(struct S) {}
void foo(struct T) {}
int main() {
S s{1, 2, 3};
foo({1, 2, 3}); // shall be ok: foo(struct T) only available
}

8 https://godbolt.org/z/z7TeGY
Ставим под сомнение интуицию
• Перегрузка работает раньше, чем разрешение private/public
class S {
void foo(int) {}
public:
void foo(short) {}
};
S s;
s.foo(1);
• Можно ли доказать это ссылаясь на стандарт?

9 https://godbolt.org/z/7s5xnM
Ставим под сомнение интуицию
• Перегрузка работает раньше, чем разрешение private/public
class S {
void foo(int) {}
public:
void foo(short) {}
};
S s;
s.foo(1);
• [class.access] In the case of overloaded function names, access control is applied
to the function selected by overload resolution

10
Ставим под сомнение интуицию
• Перегрузка работает раньше, чем анализ стёртых функций
struct S {
int foo(int) = delete;
int foo(short) {}
};
S s;
s.foo(1);
• [dcl.fct.def.delete] A program that refers to a deleted function implicitly or
explicitly, other than to declare it, is ill-formed
• If a function is overloaded, it is referenced only if the function is selected by
overload resolution
11 https://godbolt.org/z/vn7jsf
Теперь всё менее интуитивно?
• Важный промежуточный результат это окончательно себя запутать
struct S { explicit S(int, int, int) {} };
struct T { T(int, int, int) {} };
void foo(struct S) {}
void foo(struct T) {}
int main() {
S s{1, 2, 3};
foo({1, 2, 3}); // что раньше explicit или перегрузка?
}

12
Общий обзор правил перегрузки
➢ Выбирается множество перегруженных имён [over.dcl]. При этом не все
функции и прочие сущности с одинаковыми именами засчитываются за
перегруженные [over.load]
• Выбирается множество кандидатов (включая операторы, конструкторы и
т.п.)
• Из множества кандидатов выбираются жизнеспособные (viable) для данной
перегрузки [over.match.viable]
• Лучшая из жизнеспособных выбирается на основании цепочек неявных
преобразований для каждого параметра [over.best.ics]
• Если она существует, является единственной и доступна в точке вызова, то
перегрузка разрешена успешно, иначе программа ill-formed [over.match]
13
Неразличимые объявления
• Есть две большие категории
• Те, для которых перегрузка запрещена на уровне объявлений
int b();
const int b(); // error, overloading prohibited
• Те, для которых иное объявление засчитывается за ещё одно объявление
int f(int);
int f(const int); // ok, re-declares f(int)
• Логика этого процесса довольно загадочна, мы к этому может быть вернёмся
позже

14 https://godbolt.org/z/PGx4x7
Спрятанные объявления
• Вопрос для новичка: что на экране?
struct B {
void f(int) { std::cout << "B" << std::endl; }
};
struct D : B {
void f(const char*) { std::cout << "D" << std::endl; }
};
int main() {
D d; d.f(0);
}

15 https://godbolt.org/z/KvGM9a
Спрятанные объявления
• Прошлый пример может показаться странным, но давайте его упростим
void f(int) { std::cout << "B" << std::endl; }
void f(const char*) { std::cout << "D" << std::endl; }
int main() {
extern void f(const char *);
f(0); // тут всё очевидно
}
• Ключ к пониманию: [over.dcl] two function declarations of the same name refer
to the same function if they are in the same scope and [...]
• Общий scope упрощает реализацию компилятора

16
Послабление для операторов
• Именно для операторов scope не так важен, как аргументы
void operator-(A) { std::cout << "A" << std::endl; }
void operator-(B) { std::cout << "B" << std::endl; }
int main() {
extern void operator-(B); // hides name
A a;
operator-(a); // fail as expected
-a; // still ok (!)
• Вопрос для домашней работы: обоснуйте это стандартом, а потом подумайте
по человечески, можно ли придумать разумный пример ради чего это
сделано?

17 https://godbolt.org/z/rq6Tss
Общий обзор правил перегрузки
• Выбирается множество перегруженных имён [over.dcl]. При этом не все
функции и прочие сущности с одинаковыми именами засчитываются за
перегруженные [over.load]
➢ Выбирается множество кандидатов (включая операторы, конструкторы и
т.п.)
• Из множества кандидатов выбираются жизнеспособные (viable) для данной
перегрузки [over.match.viable]
• Лучшая из жизнеспособных выбирается на основании цепочек неявных
преобразований для каждого параметра [over.best.ics]
• Если она существует, является единственной и доступна в точке вызова, то
перегрузка разрешена успешно, иначе программа ill-formed [over.match]
18
Вызов функции или конструктор?
• Это место при описании разрешения перегрузки часто обходят стороной
void foo(int) { std::cout << "fn" << std::endl; }
struct foo {
foo(int) { std::cout << "ctor" << std::endl; }
};
int main() {
foo(0); // ???
foo{0}; // ???
}
• Как вы думаете? Как думает стандарт?

19 https://godbolt.org/z/7MGov5
Оператор внутри или вовне?
• Операторы-кандидаты бывают четырёх видов
1. Функции-члены a.operator@(b)
2. Свободные функции operator@(a, b)
3. Встроенные операторы (a @ b)
4. Переписанные функции (для spaceship и некоторых других)
• Все они разрешаются в определённом порядке и в этом тоже есть
определённая логика, с которой связана методическая история

20 https://godbolt.org/z/9oPv4c
Общий обзор правил перегрузки
• Выбирается множество перегруженных имён [over.dcl]. При этом не все
функции и прочие сущности с одинаковыми именами засчитываются за
перегруженные [over.load]
• Выбирается множество кандидатов (включая операторы, конструкторы и
т.п.)
➢ Из множества кандидатов выбираются жизнеспособные (viable) для данной
перегрузки [over.match.viable]
• Лучшая из жизнеспособных выбирается на основании цепочек неявных
преобразований для каждого параметра [over.best.ics]
• Если она существует, является единственной и доступна в точке вызова, то
перегрузка разрешена успешно, иначе программа ill-formed [over.match]
21
Какой из котят выживет?
• Изо всех выбранных вариантов выбираются жизнеспособные (viable)
foo(bar); // требует разрешения...
• [over.match.viable] if there are m arguments in the list, all candidate functions
having exactly m parameters are viable
• A candidate function having fewer than m parameters is viable only if it has an
ellipsis in its parameter list
• A candidate function having more than m parameters is viable only if all
parameters following the mth have default arguments
• for F to be a viable function, there shall exist for each argument an implicit
conversion sequence ([over.best.ics]) that converts that argument to the
corresponding parameter of F
22
Какой из котят выживет?
• Изо всех выбранных вариантов выбираются жизнеспособные (viable)
foo(int, float); // viable
foo(int, ...); // viable
foo(int, float, int = 0); // viable
struct S { S(int); };
foo(S, float); // viable!
int x, y;
foo(x, y);
for F to be a viable function, there shall exist for each argument an implicit
conversion sequence ([over.best.ics]) that converts that argument to the
corresponding parameter of F
23
Внезапное озарение
• Кто здесь прав, clang или gcc?
struct S { explicit S(int, int, int) {} };
struct T { T(int, int, int) {} };
void foo(struct S) {}
void foo(struct T) {}
int main() {
S s{1, 2, 3};
foo({1, 2, 3}); // shall be ok: foo(struct T) only viable
}

24 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=97220
Обсуждение
• Что здесь удивительного?
• Например тот факт, что неявные преобразования зашиты в перегрузку
удивительно глубоко
• В следующий раз мы поговорим именно о преобразованиях более подробно
и закончим рассмотрение деталей перегрузки

25

Вам также может понравиться