Discussion: String Theory

Execute the code to understand the output and gain insights into overload resolution.

Run the code

Now, it’s time to execute the code and observe the output.

Press + to interact
#include <iostream>
#include <string>
void serialize(const void*) { std::cout << "const void*"; }
void serialize(const std::string&) { std::cout << "const string&"; }
int main()
{
serialize("hello world");
}

Why does passing a string to serialize cause the overload taking a void pointer to be called rather than the overload taking a string?

Overload resolution

When we’re calling a function with multiple overloads, the compiler uses a process called overload resolution to figure out which one is the best fit. The compiler does this by attempting to convert each function argument to the corresponding parameter type for each overload. Some conversions are better than others, and the best conversion is if the argument is already of the correct type.

All the overloads where all arguments can be successfully converted are added to a set of viable functions. Then, the compiler needs to figure out which overload to select from this set. If an overload has a better conversion for at least one argument and not a worse one for any of the other arguments, this overload is deemed to be the best viable function and is selected by overload resolution. If no overload is better than all the others, the call is ill-formed and fails to compile.

Have a look at this example:

serialize(int, int); // 1
serialize(float, int); // 2

Given these two overloads, suppose we call serialize like this:

serialize(1, 2);

Both overloads of serialize are viable. But the first overload has a better conversion for the first argument (intint is better than intfloat) and not a worse conversion for the second argument (intint for both overloads), so it is selected by overload resolution as the best viable function.

Press + to interact
#include <iostream>
void serialize(int a, int b) { std::cout << "serialize(int, int) called" << std::endl; }
void serialize(float a, int b) { std::cout << "serialize(float, int) called" << std::endl; }
int main()
{
serialize(1, 2); // Should call serialize(int, int)
serialize(1.0f, 2); // Should call serialize(float, int)
}

Understanding the output

Looking at our puzzle, which is a bit simpler than the example given above, both overloads of serialize only have one parameter. The first takes a const void* and the second takes a const std::string&. What does the conversion look like for each of the overloads?

void serialize(const void*) { std::cout << "const void*"; }
void serialize(const std::string&) { std::cout << "const string&"; }

The std::string is a class in the standard library. It’ll typically allocate memory on the heap (unless the string is very small) and allow the string to grow or be otherwise modified at runtime.

serialize("hello world");

However, the "hello world" string is not a std::string but a simple string literal. String literals are plain C-style arrays of char, which get baked into the binary by the linker and cannot be modified at runtime. A string literal has the type “array of nn const char.” The string "hello world" has 11 characters plus a terminating \0, so its type is “array of 12 const char.”

Since the argument "hello world" is neither a const void* nor a std::string but an “array of 12 const char,” a conversion is needed for both overloads. If an implicit conversion exists from the argument to the parameter type, that overload is added to the set of viable functions. Otherwise, the overload is ignored.

First overload

Let’s examine the first overload and see if the “array of 12 const char” can be implicitly converted to const void*. The first thing that happens is that the array gets converted to a pointer. Any “array of NN T” can be converted to a “pointer to T” pointing to the first element. So now our “array of 12 const char” has turned into a “pointer to const char.’’

Press + to interact
Conversion sequence for void serialize(const void*)
Conversion sequence for void serialize(const void*)

Next, any “pointer to cvcv T” (where cv means const, volatile, const volatile, or none of these) can be converted to “pointer to cvcv void.” So now our “pointer to const char” has turned into a “pointer to const void,” which is exactly what the first overload expects.

Notice that no constructors or conversion functions were involved in this conversion sequence. This means it’s a standard conversion sequence and not a user-defined conversion sequence. That gets important later.

Second overload

Let’s now examine the second overload and see if our “array of 12 const char” can be converted to a “reference to const std::string.” The std::string has a constructor std::string(const char* s), which we can use. First, we convert the “array of 12 const char” to a “pointer to const char” as we did so above. Then, we pass this to the std::string constructor and get a std::string back containing a copy of the string literal. The const std::string& parameter can bind directly to our std::string argument.

Press + to interact
Conversion sequence for void serialize(const std::string&)
Conversion sequence for void serialize(const std::string&)

Notice that we had to use a constructor for this. This means it’s a user-defined conversion sequence and not a standard conversion sequence. It doesn’t matter that std::string is a standard library type; it still counts as user-defined. The rules are the same for us and the standard library.

Now the compiler has found a valid conversion sequence from our “array of 12 const char” to the parameter type of each overload and has to figure out which sequence is best.

We only require a standard conversion sequence (top) to call the const void* overload. To call the std::string overload, we need a user-defined conversion sequence (bottom), which involves creating a new temporary std::string object. A standard conversion sequence is always better than a user-defined conversion sequence, so the first overload gets called and const void* is printed.

Level up your interview prep. Join Educative to access 70+ hands-on prep courses.