05 - overloading

Sometimes you may encounter a problem where you need multiple functions doing similar task or the same task but on different types. You probably would name them similarly - in fact, names can be the same thanks to overloading.

Function overloading is a feature that allows to have multiple functions with the same name as long as their signatures are different.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

void divide(int a, int b)
{
	if (b == 0)
	{
		std::cout << "can not divide by 0\n\n";
		return;
	}

	std::cout << "quotient: " << a / b << "\n";
	std::cout << "remainder: " << a % b << "\n\n";
}

void divide(double a, double b)
{
	std::cout << "quotient: " << a / b << "\n\n";
}

int main()
{
	divide(16, 7);
	divide(13, 0);
	divide(16.0, 7.0);
	divide(13.0, 0.0);
}
quotient: 2
remainder: 2

can not divide by 0

quotient: 2.28571

quotient: inf

The first 2 calls are using the first overload and the second 2 calls are using the second one.

How it works

Which overload to call is decided upon provided arguments. In order to overload, functions must have different signature.

Examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ok: all overloads have different amount of arguments
int func1(int);
int func1(int, int);
int func1(int, int, int);

// ok: all overloads have different argument types
int func2(int);
int func2(float);
int func2(double);

// also ok
int func3(int, float);
int func3(int);
int func3(double);
int func3(char);

// error: can not overload only by different return type
int func4();
void func4();

// ok: different amount of arguments
int func5();
void func5(int);

// top-level const/volatile are ignored
// ok but these all declare the same overload 3 times
void func6(int);
void func6(const int);
void func6(volatile int);

// ok: different argument types
// (references do not have top-level cv-qualifiers)
void func7(int*);
void func7(int&);
void func7(int&&);
void func7(const int&);
void func7(const int&&);

// error: can not overload only by different exception specification
// (exceptions explained later)
void func8();
void func8() noexcept;

When a call to overloaded function is encountered, a mechanism called overload resolution takes place:

  • If no function can be matched to provided arguments and their types, the program is ill-formed (compiler error)

  • If multiple overloads match, they are selected in this order:

    • an overload which does not require any convertions (perfect match)

    • an overload which requires promotion (lossless convertion) or user-defined convertion

    • an overload which requires narrowing convertion (potentially losing convertion)

  • If multiple overloads require same-rank convertion then the one that requires least convertions is selected.

  • If there are still multiple overloads to choose from the program is ill-formed due to ambiguity.

The detailed rules are much more complicated but the points above should serve well as a general explanation.

Examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void f1(int, float);  // A
void f1(int, double); // B
void f1(long, float); // C

// error: ambiguous call
// A: requires 2x narrowing convertion (long => int, double => float)
// B: requires 1x narrowing convertion (long => int)
// C: requires 1x narrowing convertion (double => float)
f1(42l, 3.14);

void f2(unsigned); // X
void f2(double);   // Y

// error: ambiguous call
// X: requires 1x narrowing (int => unsigned)
// Y: requires 1x narrowing (int => double)
f2(0);

// ok: perfect match with X
f2(0u);

// ok: selects Y
// X: requires 1x narrowing (float => unsigned)
// Y: requires 1x promotion (float => double)
f2(3.14f);

// error: ambiguous call
// X: requires 1x narrowing (char => unsigned)
// Y: requires 1x narrowing (char => double)
f2('a');

Deleted overloads

The keyword delete is primarily for memory deallocation, but since C++11 it gained an additional functionality. It can be used to explicitly delete function overloads. Deleted overloads take part in overload resolution as any other overload, but if a deleted overload is choosen then compilation fails.

1
2
3
4
5
6
void f1(long);
f1(1); // ok, int promoted to long

void f2(int) = delete;
void f2(long);
f2(2); // chooses first overload, triggers compilation error

In practice

Don't worry if you can not memorize everything - intuitively the most fitting overload is choosen.

Operators, like functions can be overloaded too (operators actually are functions, just with special syntax). The best example is << which is extensively overloaded for standard library stream types (std::cout is one of such streams). As of C++17, there are 29 overloads, 14 of which are templates.

How do templates play with overload resolution?

Function templates, before entering overload resolution are subject to template type deduction, that is, their template parameters are sort of guessed based on provided arguments and template arguments. It's a pretty complex mechanism but after finishing it (or failing it, in the case of SFINAE, which is even more complicated) a signature is formed (or not, if SFINAE happened) that takes part in overload resolution like any other overload. If there is a match of the same priority from a template and non-template overload, the non-template one is selected.