C++ 是如何处理函数符号查找?

本文深入探讨C++中函数符号查找的过程,涉及函数重载、运算符重载、函数模板、命名空间等复杂情况。通过非限定名查找、成员查找、全限定名查找等方式解析函数调用,并讲解了ADL(Argument-Dependent Lookup)及函数模板的处理。在函数调用解析后,编译器通过参数匹配、模板实例化等步骤确定最佳匹配函数。
摘要由CSDN通过智能技术生成

在 C++ 的日常开发中,经常会遇到函数冲突或者找不到匹配的函数的问题,如果不了解编译器处理函数符号查找的行为,就很难去解决这些问题。

【参考 How C++ Resolves a Function Call - https://preshing.com/20210315/how-cpp-resolves-a-function-call/

1、函数符号查找难点

C 语言的函数调用很简单,每个函数都有唯一的名字。

但 C++ 就复杂得多了,因为其有:

  • 函数重载 (overloading)
  • 运算符重载 (built-in operators)
  • 函数模版 (function templates)
  • 命名空间 (namespaces)

2、函数符号查找解析过程

那么 c++ 编译器是如何查找符号的,一图以蔽之。

 

 

3、Name Lookup

 

 

在符号查找 (name lookup) 阶段,会有三种主要的方式:

  • Member name lookup (成员符号查找)
    • 发生在符号是在. 或 -> 标识符的右边,比如 foo->bar。这种方式是用来定位类的成员符号。
  • Qualified name lookup (全限定名查找)
    • 发生在符号带有 :: 标识符的情况,比如std::sort。这种方式的符号查找是非常明确、显式声明的。在 :: 标识符右边的符号只会在标识符左边的作用域内查找。
  • Unqualified name lookup (非限定名查找)
    • 不是上边的那两种。当编译器看到一个非限定名,它会根据上下文信息在各种各样的作用域内来查找能够匹配的声明。

4、Unqualified name lookup

【参考 https://en.cppreference.com/w/cpp/language/unqualified_lookup

【注:只摘取部分查找符号的情况,感兴趣可以直接看参考链接原文】

4.1、File scope

在全局作用域下,即不处于任何其他作用域(函数、类、自定义命名空间)下,符号在使用之前会被校验。

int n = 1;     // declaration of n
int x = n + 1; // OK: lookup finds ::n

int z = y - 1; // Error: lookup fails
int y = 2;     // declaration of y

4.2、Namespace scope

在自定义的命名空间下,当前的命名空间在符号被使用之前的那部分会先被查找,然后再一直向外层命名空间的前面部分查找,知道找到或者到达了全局作用域。

int n = 1; // declaration
namespace N {
  int m = 2;
  namespace Y {
    int x = n; // OK, lookup finds ::n
    int y = m; // OK, lookup finds ::N::m
    int z = k; // Error: lookup fails
  }
  int k = 3;
}

4.3、Definition outside of its namespace

对于使用命名空间下的符号,其符号查找也会先从命名空间下去查找。

namespace X {
    extern int x; // declaration, not definition
    int n = 1; // found 1st
};
int n = 2; // found 2nd.
int X::x = n; // finds X::n, sets X::x to 1

4.4、Non-member function definition

函数定义里的符号查找,会先从其 block 块内进行查找,然后就是更外层的 block 块内,最后才会去查找其相关的命名空间。

namespace A {
   namespace N {
       void f();
       int i=3; // found 3rd (if 2nd is not present)
    }
    int i=4; // found 4th (if 3rd is not present)
}

int i=5; // found 5th (if 4th is not present)

void A::N::f() {
    int i = 2; // found 2nd (if 1st is not present)
    while(true) {
       int i = 1; // found 1st: lookup is done
       std::cout << i;
    }
}

// int i; // not found

namespace A {
  namespace N {
    // int i; // not found
  }
}

4.5、Class definition

namespace M {
    // const int i = 1; // never found
    class B {
        // static const int i = 3; // found 3rd (but later rejected by access check)
    };
}
// const int i = 5; // found 5th
namespace N {
    // const int i = 4; // found 4th
    class Y : public M::B {
        // static const int i = 2; // found 2nd
        class X {
            // static const int i = 1; // found 1st
            int a[i]; // use of i
            // static const int i = 1; // never found
        };
        // static const int i = 2; // never found
    };
    // const int i = 4; // never found
}
// const int i = 5; // never found

4.6、Default argument

这里函数定义的默认参数,会先从函数的参数开始查找。

class X {
    int a, b, i, j;
public:
    const int& r;
    X(int i): r(a), // initializes X::r to refer to X::a
              b(i), // initializes X::b to the value of the parameter i
              i(i), // initializes X::i to the value of the parameter i
              j(this->i) // initializes X::j to the value of X::i
    { }
}
 
int a;
int f(int a, int b = a); // error: lookup for a finds the parameter a, not ::a
                         // and parameters are not allowed as default arguments

4.7、Catch clause of a function-try block

在函数的 try-catch 里的符号使用,首先查找当前 block,然后会看函数参数,最后才是外层的。

int n = 3; // found 3rd
int f(int n = 2) // found 2nd
try {
   int n = -1;  // never found
} catch(...) {
   // int n = 1; // found 1st
   assert(n == 2); // loookup for n finds function parameter f
   throw;
}

注意:这里的 try-catch 是函数的定义。

#include <iostream>
#include <string>
struct S {
    std::string m;
    S(const std::string& str, int idx) try : m(str, idx) {
        std::cout << "S(" << str << ", " << idx << ") constructed, m = " << m << '\n';
    } catch(const std::exception& e) {
        std::cout << "S(" << str << ", " << idx << ") failed: " << e.what() << '\n';
    } // implicit "throw;" here
};
int main() {
    S s1{"ABC", 1}; // does not throw (index is in bounds)
    try {
        S s2{"ABC", 4}; // throws (out of bounds)
    } catch (std::exception& e) {
        std::cout << "S s2... raised an exception: " << e.what() << '\n';
    }
}
/* Output:
S(ABC, 1) constructed, m = BC
S(ABC, 4) failed: basic_string::basic_string: __pos (which is 4) > this->size() (which is 3)
S s2... raised an exception: basic_string::basic_string: __pos (which is 4) > this->size() (which is 3)
*/

4.8、Overloaded operator

当采用表达式的方式时,比如 a + a,那么会分别进行两种查找:1、非成员运算符重载;2、成员运算符重载。最终这两种找到的符号会并入 build-in 内置的运算符中形成一个集合,最后在其中进行匹配。

而当采用方法来进行调用时,就会走正常的非限定名查找方式。

struct A {};
void operator+(A, A); // user-defined non-member operator+
 
struct B {
    void operator+(B); // user-defined member operator+
    void f ();
};
 
A a;
 
void B::f() // definition of a member function of B
{
    operator+(a,a); // error: regular name lookup from a member function
                    // finds the declaration of operator+ in the scope of B
                    // and stops there, never reaching the global scope
    a + a; // OK: member lookup finds B::operator+, non-member lookup
           // finds ::operator+(A,A), overload resolution selects ::operator+(A,A)
}

5、Argument-dependent lookup

【参考:https://en.cppreference.com/w/cpp/language/adl

Argument-dependent lookup,俗称 ADL,是用在函数调用表达式的非限定名的查找规则,包括重载运算符的调用。

#include <iostream>
int main()
{
    std::cout << "Test\n"; // There is no operator<< in global namespace, but ADL
                           // examines std namespace because the left argument is in
                           // std and finds std::operator<<(std::ostream&, const char*)
    operator<<(std::cout, "Test\n"); // same, using function call notation
 
    // however,
    std::cout << endl; // Error: 'endl' is not declared in this namespace.
                       // This is not a function call to endl(), so ADL does not apply
 
    endl(std::cout); // OK: this is a function call: ADL examines std namespace
                     // because the argument of endl is in std, and finds std::endl
 
    (endl)(std::cout); // Error: 'endl' is not declared in this namespace.
                       // The sub-expression (endl) is not a function call expression
}

5.1、Detail

首先,ADL 在以下的非限定名查找中不会生效:

1) 类成员的声明

2) 块内的函数声明

3) 非函数声明,或者函数模版

上述情况会使用常规的 unqualified lookup,否则对于函数调用表达式,会检查它的每一个参数来判断相关的命名空间和类,然后把他们加进 lookup 的集合当中。

 

 

6、Special Handling of Function Templates

对于函数模版而言,有一个问题:就是你不能直接调用它。因此,在符号查找完成后,编译器会遍历候选符号集合,会尝试把函数模版转换成函数。

 

 

6.1、template argument deduction

 

 

这个函数有一个模版参数 T,template argument deduction (模版参数推断) 会进行操作,编译器会比较调用者的参数和模版参数的类型,如果正确能推断,则模版参数 T 会被推断成一种类型。

比如这里的模版参数 T 被推断为 galaxy::Asteroid

 

 

如果无法正确推断,则这个函数模版会被候选符号集合去掉。

6.2、template argument substitution

所有在候选符号集合里面存活的函数模版,会进入下一个阶段 template argument substitution (模版参数替换)

在这个阶段,模版参数 T 直接被替换为 galaxy::Asteroid

 

 

当然,也会存在模版参数替换失败的情况,比如下面这种情况,要求 T 还存在 Units 的成员:

 

 

当模版参数替换失败时,函数模版会被候选符号集合去掉。

6.3、SFINAE

利用函数模版的特性,形成了元编程的技术,比如 https://en.cppreference.com/w/cpp/language/sfinae (substitution failure is not an error),正如上面的操作,替换失败并不会造成编译报错。

这样就能在编译期间完成一些计算的需求,且最终让相关操作落到我们想要的逻辑当中。

template <int I> void div(char(*)[I % 2 == 0] = 0) {
    // this overload is selected when I is even
}
template <int I> void div(char(*)[I % 2 == 1] = 0) {
    // this overload is selected when I is odd
}

不过在 modern c++ 中,因为出现了 constraints 和 constexpr if 同样能够满足开发者的需要。

7、candidate functions

接下来需要从一众候选符号集合中,选出可行的符号。

 

 

最明显的需求就是参数需要能够匹配。至少要能进行隐式转换的。

 

 

 

 

 

 

c++20 还会有一个 constraints 的特性,用于自定义逻辑来排除一些模版,所以还需要查看是否满足其要求。

8、决斗时刻

最终还是剩下一些候选符号,编译器需要决出最佳匹配的可行的函数。

 

 

首先是参数能更好匹配的胜出,编译器偏好于需要进行更少的隐式转换的函数。

当然,如果需要进行隐式转换,某一些转换也会优于另外一些转换,参考 Ranking of implicit conversion sequences

然后,编译器会偏好于非模版函数;

最后,编译器会偏好于更能确定 (more specialized) 的函数,这里面也有一些规则的判断,参考 rules to decide which function template is more specialized than another

9、After the Function Call Is Resolved

在决出要调用哪个函数之后,编译器还会有一些工作需要去完成:

  • 如果函数是类成员,编译器还需要去检查成员的访问属性 (access specifiers),如 private、protected 等。
  • 如果函数是模版,如果它的定义是可见的,则编译器需要去实例化 (instantiate) 函数模版。
  • 如果函数是虚函数(virtual function),那么编译器需要去生成特别的机器指令辅助运行时找到正确的函数。

参考链接

虽然我在写每一个点的时候直接把参考链接放在里面,这里还是再列一遍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值