Translate articles. Function call

C is a simple language. A function can only correspond to one variable name. C++ offers greater flexibility:

  • Can have more than one function with the same name (overload).
  • Built-in operators such as+and= =
  • A function template
  • Use namespaces to avoid naming conflicts.

With these capabilities, you can return a concatenation of two strings str1 + str2, and you can overload functions to handle different types.

However, it is prone to problems when used. At some point, the compiler may report an error:

error C2666: 'String::operator ==': 2 overloads have similar conversions
note: could be 'bool String::operator ==(const String &) const'
note: or       'built-in C++ operator==(const char *, const char *)'
note: while trying to match the argument list '(const String, const char *)'
Copy the code

Like many C++ programmers, I’ve struggled with mistakes like this my entire career. Each time this happens, I usually search the web and change the code until it compiles.

Fortunately, it’s 2021, and information about C++ is more comprehensive than ever before. Thanks in particular to cppreference.com, I now know what was missing from my understanding: a panorama of each function call.

Which function is selected by the compiler when calling a function:

In order to prevent different translation habits of everyone, the original picture is put here. The following steps are specific for translating diagrams into Chinese.

These steps are incorporated into the C++ standard. Every C++ compiler must follow them, and the process of selecting specific functions based on function calls occurs at compile time. It is clear that such an algorithm is necessary to support all of the above features of C++.

Algorithms boil down to “doing what the programmer expects”, and most of the time that’s true, but it’s a big mistake not to consider the algorithm as a whole. When you start using more than one C++ feature, such as a development library, it’s a good idea to know something about it.

Let’s walk through the algorithm from beginning to end. Some of the things we’re talking about will be familiar to experienced C++ programmers. Nonetheless, it is helpful to look at the overall steps. We’ll cover several advanced C++ subtopics, such as finding dependent arguments and SFINAE, but we won’t delve into any specific subtopics. That way, even if you don’t know anything about the subtopic, you’ll at least know how it implements functionality that meets C++ ‘s overall strategy for resolving function calls at compile time.

Function name lookup

Our journey begins with a function call. The following function BLAST (AST, 100) is an example. It is clearly intended to call a function called BLAST. But which one?

namespace galaxy {
    struct Asteroid {
        float radius = 12;
    };
    void blast(Asteroid* ast, float force);
}

struct Target {
    galaxy::Asteroid* ast;
    Target(galaxy::Asteroid* ast) : ast{ast} {}
    operator galaxy::Asteroid*() const { returnast; }};bool blast(Target target);
template <typename T> void blast(T* obj, float force);

void play(galaxy::Asteroid* ast) {
    blast(ast, 100);
}
Copy the code

The first step in answering this question is to look up the function name. In this step, the compiler looks at all functions and function templates prior to the function call and filters out those that match the function name.

As the flowchart shows, there are three main types of lookup function names, each with its own set of rules.

  • When the name is located.or->The right side of the symbol is triggeredMember function name lookup, e.g.foo->barUsed to locate class members.
  • Restricted function name lookupIt’s something that has a namespace, which is theta: :Symbolic, for examplestd::sort. This type of variable name is obvious,: :The variable name on the right just looks up the current namespace.
  • Unrestricted function name lookup. When the compiler sees such a function name, such as a loneblastI’m going to see what’s next. A set ofDetailed rulesTo determine exactly where the compiler should look.

For the previous example, this is an unrestricted function name. When performing a function name lookup for a function call, the compiler may find multiple declarations. Let’s call these statements candidates. In the above example, the compiler looks for three candidates:

The first candidate, the red circle, is noteworthy because it shows an easily overlooked feature of C++ : an argument-dependent lookup, or ADL for short. Normally, you would not consider this function as a candidate for the current function call lookup, because it is declared in the namespace Galaxy and the function call comes from outside the namespace. There is also no using Namespace Galaxy in the code. So why is this function a candidate?

This is because ADL is triggered when you use an unrestricted function call and the function name is not a class member, and function name lookup becomes more “greedy”. In addition to the usual locations, the compiler also looks for candidate functions in the parameter namespace, hence the term “parameter dependent lookup.”

This example, for example, also looks for candidate functions for the Galaxy namespace. You can make ADL implement some convenient functions, such as +, ==.

Special handling of function templates

Some of the candidates found by the function name lookup are functions, and some are function _ template _. ** Function templates have one problem: you can’t call them. ** We can only call functions. Therefore, after looking up the function name, the compiler looks through the candidate list to try to convert the function template into a function.

As in the previous example, one candidate is a function template:

This function template takes one template argument. Our function call BLAST (AST, 100) doesn’t specify any template parameters, so to convert this function template to a function, the compiler has to figure out its type, which is where template argument deduction comes in. In this step, the compiler compares the type of argument passed by the function call (left in the figure below) with the type of argument expected by the function template (right). If there are template parameters on the right that do not match, such as T, the compiler tries to infer them using the information on the left.

In this example, the compiler extrapolates T to Galaxy ::Asteroid because it makes the first function argument T* correspond to ast. The rules governing the deduction of template parameters are a big topic in and of themselves, but in a simple example like this, it usually does what you expect. If template argument inference does not work (in other words, the compiler cannot infer template arguments in a way that makes the function arguments correspond to the parameters of the function call), the function template is removed from the candidate list.

Then comes the next step: template parameter substitution. In this step, the compiler replaces general function template arguments with concrete ones. In the example above, it is the template parameter T that is replaced by the template parameter Galaxy ::Asteroid derived from it. When this step succeeds, we finally have a real function that can be called, not just a function template!

Of course, in some cases, template parameter substitution may fail. Suppose the same function template accepts a third argument, as follows:

template <typename T> void blast(T* obj, float force, typename T::Units mass = 5000);
Copy the code

If so, the compiler tries to replace T in T::Units with Galaxy ::Asteroid, but there is no Galaxy ::Asteroid::Units. So template parameter replacement will fail.

When template parameter replacement fails, the function template is removed from the candidate list. At some point in C++ history, people realized that this was also a feature that could be exploited. This discovery in itself led to a whole set of metaprogramming techniques collectively known as SFINAE. SFINAE is a complicated thing, and I’ll just say two things here. First, it is essentially a way to manipulate the function call resolution process to select the candidate you want. Second, it may fall out of favor over time, as programmers increasingly turn to modern C++ metaprogramming techniques to achieve the same thing entirely with other things, such as l constraints and constexpr if.

Resolution of overload

At this stage, the templates found in the function name lookup are gone (deductively replaced or eliminated), leaving only a clean set of candidate functions. This is also known as an overloaded set. Here is a list of candidate functions for the previous example:

At this stage, all the function templates discovered during the name lookup are gone, and we are left with a nice, clean set of candidate functions. This is also known as an overload set. Here is a list of candidate feature updates for our example:

The next two steps narrow the list further by determining which candidate functions are viable (in other words, which can handle function calls).

The most obvious requirement is that the arguments must be compatible: that is, a viable function should be able to accept the arguments of a function call. If the parameter types of a function call do not exactly match the parameter types of the function, at least an implicit conversion can be used to convert each parameter to the corresponding parameter type. Let’s see if the arguments for the candidate function in this example are compatible:

Candidate for 1

The first parameter type matches exactly, and the second parameter type int can be implicitly converted to the second function parameter type float. Therefore, the parameters of candidate 1 are compatible.

Candidate for 2

The first parameter type is implicitly converted to the first function parameter type because there is a conversion constructor that accepts arguments of type Galaxy ::Asteroid*. That is:

struct Target {
    galaxy::Asteroid* ast;
    Target(galaxy::Asteroid* ast) : ast{ast} {}
    operator galaxy::Asteroid*() const { returnast; }};Copy the code

However, the function call takes two arguments, and candidate 2 accepts only one. Therefore, candidate 2 is not feasible.

Candidate for 3

The parameter types are exactly the same, so it’s also compatible.

Implicit conversions are used in part of this process, and you can declare explict to prohibit implicit conversions.

Final moment

In the example above, there are two possible functions. Either of them would have handled the original function call just fine:

So who? I’m not the only guy in the world who’s attracted to two functions!

The compiler now has several possible function choices: it must choose the best one. To be the best possible function, it must “beat” all the other possible functions, and this is determined by a set of rules.

Let’s look at the first three rules.

The first rule: the one who matches the parameters better wins

C++ pays most attention to the degree to which function call parameter types match function parameter types. It prefers functions with fewer implicit conversions. When both functions require conversions, some conversions are considered “better” than others. For example, whether the STD ::vector operator [] is a const or non-const version.

In the previous example, two viable functions with the same parameter type, tie, down to the second rule.

Second rule: non-template functions win

If the first rule doesn’t solve it, C++ prefers to call non-template functions rather than template functions.

Is obvious:

It’s worth reiterating that the first two ties were ordered the way I described. In other words, if there is a viable function whose arguments match the given parameters better than all the other viable functions, it wins, even if it is a template function.

Rule 3: More detailed templates win

In our example, the best possible function has been found, but if not, we move on to the third rule. C++ prefers “more refined” template functions. For example, consider the following two function templates:

template <typename T> void blast(T obj, float force);
template <typename T> void blast(T* obj, float force);
Copy the code

When the template argument deduction is performed on these two function templates, the first function template accepts any type as its first argument, but the second function template accepts only pointer types. So the second function template is more detailed. If these two function templates are the only results of the BLAST (AST, 100) function name lookup, and both are viable, the current rule selects the second template. Deciding which function template is more detailed than another is another big issue.

However, be aware that the second function template is not actually a partial refinement of the first. Instead, they are two completely independent function templates that just happen to share the same name. In other words, they’re overloaded. C++ does not allow partial refinement of function templates.

Needless to say, if the compiler does not find a clear winner, the compile fails with an error message similar to the one at the beginning.

At this point, the panoramic analysis is complete, and then take a look at this picture at a glance.