One of the common lessons in C++ is how to overload the insertion operator (<<) when we create our own type. We are told to create a global function named operator<<
that has two params, an ostream
ref and our type and returns an ostream
ref.
std::ostream& operator<<(std::ostream& os, const MyType& mt) {
...
}
Now this makes sense when you learn that operators can be called explicitly as functions. For example, the pair of statements are functionally equivalent:
MyType mt;
std::cout << mt;
operator<<(std::cout, mt);
What's interesting though is how insertion works for built-in types. Let's consider strings, ints and even std::endl
(which until recently I assumed was a type)
std::cout << "Hello world";
operator<<(std::cout, "Hello world"); // fine
std::cout << 5;
operator<<(std::cout, 5); // ERROR!
std::cout << std::endl;
operator<<(std::cout, std::endl); // ERROR!
There is a global operator<< defined for strings (char arrays) but not for ints or std::endl
. If you want to explicitly insert those types, you have to do something different (which incidentally won't work for strings).
std::cout << "Hello world";
std::cout.operator<<("Hello world"); // ERROR!
std::cout << 5;
std::cout.operator<<(5); // fine
std::cout << std::endl;
std::cout.operator<<(std::endl); // fine
std::endl(std::cout); // also equivalent
Int handling is defined as a member function on an ostream and std::endl
is actually a function that takes advantage of a function template on ostream.
So my question is how does C++ compilation end up selecting the right function when converting the syntactic sugar for insertion operators? When a developer writes the following, what goes on under the hood to correctly lookup and map to the appropriate function addresses?
std::cout << "Id: " << 5 << std::endl;