C++泛型编程指南==》模板重载决议

与普通函数一样,函数模板也可以重载。可以使用相同的函数名来声明不同的函数体,当使用该函数名称时,编译器会决定调用哪一个候选函数。即使没有模板,这个决策规则也会相当复杂。
本节中,将讨论涉及模板的重载。

文章目录

1.何时应用重载解析

在C++中,重载解析是函数调用处理的一个重要部分,但并非每个函数调用都需要经过这一过程。重载解析主要应用于命名函数的调用,而不适用于通过函数指针或指向成员函数的指针进行的调用,也不适用于类函数宏。(因为要调用的函数完全(在运行时)由指针决定)下面是对这一过程的高层次描述:

函数调用的处理过程

  1. 查找名称以形成初始重载集:

    • 查找指定的函数名称,形成一个包含所有可能候选者的集合。
  2. 调整初始集合:

    • 若有必要,此集合会进一步调整,包括模板参数的推导和替换,这可能会导致某些函数模板候选者被排除在外。
  3. 移除完全不匹配的候选:

    • 考虑隐式转换和默认参数后,移除那些完全不匹配的候选者。这一步骤产生了一个匹配候选函数的集合。
  4. 执行重载解析以找到最佳候选:

    • 从匹配候选函数的集合中选出最佳匹配的函数。如果有唯一最佳匹配,则选定该函数;如果没有,则调用被视为具有歧义。
  5. 检查选中的候选:

    • 检查选中的候选是否是一个已删除函数(使用=delete定义的函数)或是不可访问的私有成员函数。如果是这样,编译器会发出错误信息。

总结

  • 重载解析仅应用于命名函数的调用。
  • 通过函数指针或指向成员函数的指针进行的调用不适用重载解析。
  • 类函数宏不支持重载,因此不需要进行重载解析。
  • 重载解析的目标是确定最佳匹配的函数,以便于正确地调用。

1.2 非模板类型 匹配匹配程度的排序

[[#详细解释]]

  1. 完美匹配 (1)

    • 参数具有表达式的类型,或者具有指向表达式类型的引用类型(可能添加了 const 和/或 volatile 限定符)。
    • 这种情况下不需要任何转换。
    示例代码:
    void perfectMatch(int x);  // 函数声明
    void perfectMatch(const int& x);  // 函数声明
    
    int main() {
        int y = 10;
        perfectMatch(y);  // 完美匹配 int x
        perfectMatch(y);  // 完美匹配 const int& x
        return 0;
    }
    
  2. 匹配需要微调 (2)

    • 将数组变量衰减为指向其第一个元素的指针。
    • const 添加以匹配 int* 类型的参数和 const int* 类型的参数。
    • 这些微调并不涉及类型转换,而是指针和数组的特殊规则。
    示例代码:
    void decayArray(int arr[]);  // 函数声明
    void addConst(const int* p);  // 函数声明
    
    int main() {
        int array[5] = {1, 2, 3, 4, 5};
        decayArray(array);  // 数组衰减为指针
        int* ptr = &array;  // 指向数组的指针
        addConst(ptr);  // 添加 const
        return 0;
    }
    
  3. 类型提升匹配 (3)

    • 类型提升是一种隐式转换,包括:
      • 将小整型(如 bool, char, short, 有时还包括 enum)转换为 intunsigned intlongunsigned long
      • float 转换为 double
    示例代码:
    void typePromotion(short s, float f);  // 函数声明
    
    int main() {
        short smallInt = 10;
        float floatVal = 10.0f;
        typePromotion(smallInt, floatVal);  // 类型提升
        return 0;
    }
    
  4. 标准转换匹配 (4)

    • 任何类型的标准转换(如 intfloat),或从派生类到其公共、显式基类的转换。
    • 不包括对转换运算符或转换构造函数的隐式调用。
    示例代码:
    class Base {};
    class Derived : public Base {};
    
    void standardConversion(int i, Base* b);  // 函数声明
    
    int main() {
        int intValue = 10;
        Derived derivedObj;
        standardConversion(intValue, &derivedObj);  // 标准转换
        return 0;
    }
    
  5. 用户定义的转换匹配 (5)

    • 允许任何类型的隐式转换,包括通过构造函数或转换运算符进行的转换。
    示例代码:
    class MyString {
    public:
        MyString(const char* str) {}
    };
    
    void userDefinedConversion(MyString ms);  // 函数声明
    
    int main() {
        const char* cStr = "Hello";
        userDefinedConversion(cStr);  // 用户定义的转换
        return 0;
    }
    
  6. 省略号匹配 (6)

    • 省略号参数几乎可以匹配任何类型,但有一个例外:具有重要复制构造函数的类型可能是有效的,也可能不是有效的(实现可以允许或禁止)。
    示例代码:
    void variadicArgs(...);  // 函数声明
    
    int main() {
        int someInt = 10;
        variadicArgs(someInt);  // 省略号匹配
        return 0;
    }
    

总结

  • 完美匹配 (1):参数类型与函数参数类型完全相同,或者参数是左值引用且指向相同类型的对象。
  • 匹配需要微调 (2):数组变量衰减为指针,或添加 const 限定符以匹配参数类型。
  • 类型提升匹配 (3):小整型和浮点型的自动提升。
  • 标准转换匹配 (4):标准转换或派生类到基类的转换。
  • 用户定义的转换匹配 (5):通过构造函数或转换运算符进行的转换。
  • 省略号匹配 (6):省略号参数可以匹配多种类型,但对于某些类型可能有限制。

这些示例展示了C++中不同类型匹配的具体情况及其优先级。

1.3 重载解析发生在模板参数推导后,而且推导不考虑所有这些类型的转换。

在C++中,模板参数推导发生在重载解析之前,并且在推导过程中不考虑所有类型的转换。下面是一个具体的例子来说明这一点:

template<typename T>
class MyString {
public:
    MyString(T const*);  // 转换构造函数
};

template<typename T>
MyString<T> truncate(MyString<T> const&, int);

int main() {
    MyString<char> str1, str2;

    str1 = truncate<char>("Hello World", 5);  // OK
    str2 = truncate("Hello World", 5);        // ERROR
}

详细解释

  1. 模板参数推导:

    • 当你调用一个模板函数时,编译器首先尝试通过模板参数推导来确定模板实参。
    • 在这个例子中,当我们调用 truncate<char>("Hello World", 5) 时,编译器会尝试推导出模板参数 <T> 应该是什么类型。
  2. 模板参数推导不考虑转换构造函数:

    • 在模板参数推导的过程中,编译器不会考虑通过转换构造函数提供的隐式转换。
    • 因此,在 str2 = truncate("Hello World", 5); 这一行中,编译器试图推导出模板参数 <T> 的类型,但它不会考虑 MyString 的转换构造函数。
  3. 重载解析发生在模板参数推导之后:

    • 一旦模板参数被推导出来,编译器就会开始进行重载解析。
    • str1 = truncate<char>("Hello World", 5); 中,模板参数 <T> 明确地被指定为 <char>,因此可以调用 MyString<char> 的转换构造函数将 "Hello World" 转换为 MyString<char> 类型。
  4. 错误的情况:

    • 对于 str2 = truncate("Hello World", 5); 这一行,编译器试图推导出模板参数 <T> 的类型。
    • 但是,由于模板参数推导不考虑转换构造函数,因此它无法确定 <T> 应该是什么类型。
    • 因此,编译器找不到一个可行的函数来调用,从而不执行重载解析,并报告一个错误。

总结

  • 模板参数推导发生在重载解析之前。
  • 模板参数推导不考虑通过转换构造函数提供的隐式转换。
  • 如果模板参数推导失败,那么不会进行重载解析。
  • 在示例中,str1 = truncate<char>("Hello World", 5); 成功,因为模板参数 <T> 明确指定,且可以使用转换构造函数。
  • str2 = truncate("Hello World", 5); 失败,因为编译器无法推导出 <T> 的类型,因此不执行重载解析。

这个例子展示了模板参数推导和重载解析之间的交互,以及在模板参数推导过程中不考虑转换构造函数的重要性。

1.4引用类型的模板参数推导规则

在C++中,模板参数推导是在调用模板函数时进行的,并且有一些特殊的规则适用于引用类型的推导。下面是一些具体的例子来说明这些规则:

template<typename T>
void strange(T&&, T&&);

template<typename T>
void bizarre(T&&, double&&);

int main() {
    strange(1.2, 3.4);  // OK: with T deduced to double
    double val = 1.2;
    strange(val, val);  // OK: with T deduced to double&
    strange(val, 3.4);  // ERROR: conflicting deductions
    bizarre(val, val);  // ERROR: lvalue val doesn't match double&&
}

详细解释

  1. 模板参数推导规则:

    • 当模板参数是引用类型时,模板参数推导有一些特殊的规则。
    • 如果模板参数是右值引用 T&&,并且传递给它的参数是一个左值,则模板参数会被推导为该左值的引用类型 T&
  2. strange(1.2, 3.4);

    • 在这个例子中,两个参数都是临时值,因此模板参数 T 被推导为 double
    • 因此,strange<double&&, double&&> 被实例化为 strange<double&, double&>
  3. strange(val, val);

    • 在这里,val 是一个左值,因此模板参数 T 被推导为 double&
    • 因此,strange<double&, double&> 被实例化,这与传递的参数类型匹配。
  4. strange(val, 3.4);

    • 在这个例子中,val 是一个左值,因此 T 被推导为 double&
    • 但是,第二个参数 3.4 是一个临时值,因此它会被推导为 double
    • 这里产生了冲突,因为一个模板参数被推导为 double&,而另一个被推导为 double,导致推导失败。
  5. bizarre(val, val);

    • 在这里,val 是一个左值,而模板参数要求 T&&double&&
    • 因此,T 被推导为 double&,但这与 double&& 不匹配,导致推导失败。

总结

  • 当模板参数是右值引用 T&& 时,如果传递的参数是一个左值,则模板参数会被推导为该左值的引用类型 T&
  • 如果一个模板函数调用中有多个参数,并且这些参数导致不同的模板参数推导结果,那么这个函数调用会导致错误。
  • 在上述示例中,strange(1.2, 3.4)strange(val, val) 成功,因为模板参数可以被一致地推导。
  • strange(val, 3.4) 失败,因为模板参数推导产生冲突。
  • bizarre(val, val) 失败,因为左值 valdouble&& 不匹配。

这些规则和示例展示了模板参数推导在处理引用类型时的细节。

1.5 C++中的成员函数隐含参数与重载解析

在C++中,非静态成员函数的调用具有一个隐含参数 this,成员函数的定义中可以访问该参数。对于类 MyClass 的成员函数,隐含参数的类型通常是 MyClass&(用于非 const 成员函数)或 MyClass const&(用于 const 成员函数)。如果成员函数是 volatile 类型,则隐含参数可以是 MyClass volatile&MyClass const volatile& 类型,但这种情况较为罕见。

隐含参数 this 的类型
  • 对于非 const 成员函数,隐含参数的类型为 MyClass&
  • 对于 const 成员函数,隐含参数的类型为 MyClass const&
  • 对于 volatileconst volatile 成员函数,隐含参数的类型分别为 MyClass volatile&MyClass const volatile&
隐含参数 this 的参与重载解析

隐含参数 this 与显式参数一样参与重载解析。然而,在某些情况下,这可能导致意外的结果。下面是一个示例,展示了一个类似字符串的类 Badstring,其成员函数的行为不符合预期:

#include <cstddef>

class Badstring {
public:
    Badstring(char const*);
    char& operator[](std::size_t);              // #1
    char const& operator[](std::size_t) const;  // #2
    operator char*();                           // #3
    operator char const*() const;               // #4
};

int main() {
    Badstring str("correct");
    str[5] = 'c';  // 可能存在重载解析的歧义!
}
重载解析歧义分析
  • 表达式 str[5] 似乎没有任何问题。在 #1 处的下标操作符似乎是完美的匹配。
  • 但实际上,参数 5int 类型,而操作符需要无符号整型(std::size_t 通常为 unsigned int 类型或 unsigned long 类型,但永远不会是 int 类型)。
  • 尽管如此,一个简单的标准整数转换使得 #1 很容易实现。
  • 另外还有一个内置下标操作符。若将隐式转换操作符应用于 str(隐式成员函数参数),就会获得指针类型,现在内置下标操作符也会应用。这个内置操作符接受一个 ptrdiff_t 类型的参数,该参数在许多平台上等价于 int 类型,因此参数 5 完美匹配。
  • 因此,尽管内置下标操作符对隐含参数 this 匹配较差(通过用户定义的转换),但比在 #1 处定义的操作符对实际下标表达式的匹配更好!因此,可能存在歧义。
  • 这种歧义只存在于 std::size_tunsigned int 的平台上。在它是 unsigned long 平台上,类型 ptrdiff_tlong 类型的别名,并且不存在歧义,因为内置的下标操作符也需要对下标表达式进行转换。
解决方案

为了可移植地解决这类问题,可以采用以下方法之一:

  1. 使用 ptrdiff_t 类型参数声明 operator[]
  2. 使用显式转换来替换隐式到 char* 的类型转换(这是推荐的做法)。
C++11 中的 this 参数类型

C++11 引入了语法来明确指定隐含参数 this 的类型:

  • 默认情况下,非静态成员函数有一个隐含的 this 参数,是左值引用类型。
  • 可以使用 &&& 后缀来明确指定隐含参数 this 的类型为左值引用或右值引用。
struct S {
    void f1();  // 隐含的 `this` 参数是左值引用
    void f2()&; // 隐含的 `this` 参数是右值引用
    void f3()&; // 隐含的 `this` 参数是左值引用
};

int main() {
    S().f1();  // OK: 旧规则允许右值 `S()` 匹配隐含的左值引用类型 `S&` 的 `this`
    S().f2();  // OK: 右值 `S()` 匹配右值引用类型 `S&&` 的 `this`
    S().f3();  // ERROR: 右值 `S()` 不能匹配显式请求的左值引用类型 `S&` 的 `this`
}

在这个例子中,可以看到不仅可以通过使用 && 后缀使隐含参数成为右值引用,还可以通过使用 & 后缀确认左值引用情况。然而,显式请求左值引用处理时,旧的特殊情况(允许右值绑定到非 const 类型的左值引用)不再适用,这可能会导致编译错误。

1.6 C++中的非模板函数与模板函数的选择

在C++中,当进行重载解析时,如果所有其他条件相同,非模板函数会优先于模板函数。这意味着即使模板实例可能更好地匹配给定的实参,编译器也会选择非模板函数。这里有一个例子来说明这一点:

template<typename T>
int f(T);  // #1 - 泛型模板函数

void f(int);  // #2 - 非模板函数

int main() {
    return f(7);  // ERROR: selects #2, which doesn't return a value
}

在这个例子中,f(7) 的调用选择了 #2 而不是 #1,即使 #1 可以接受任何类型的参数并且在给定 int 类型的情况下会生成一个更好的匹配实例。这是因为 #2 是一个非模板函数,而非模板函数在所有条件相同的情况下优先于模板函数。

重载解析不考虑返回类型

值得注意的是,重载解析通常不涉及所选函数的返回类型。在上述例子中,虽然 #2 不返回一个值(而 #1 返回一个 int 值),但是编译器仍然选择了 #2。这是因为返回类型不是选择最佳函数重载的一部分。

成员函数与构造函数

当成员函数定义为接受与复制或移动构造函数相同的参数时,这种选择机制可能会导致不可预期的行为。这是因为复制和移动构造函数通常用于创建新对象,而成员函数则用于已存在的对象。

最特化的模板选择

如果需要在两个模板之间进行选择,首选最特化的模板(前提是其中一个实际上比另一个更特化)。这通常发生在当一个模板被显式特化时,显式特化的模板将优先于泛型模板。

参数包的情况

当两个模板仅在是否包含参数包(...)上有区别时,没有参数包的模板被认为是更特化的。例如:

template<typename T>
void f(T);

template<typename T, typename... Args>
void f(T, Args...);

在这个例子中,第一个模板 f(T) 没有参数包,而第二个模板 f(T, Args...) 包含一个参数包。如果没有参数包的模板能够匹配调用,那么它会被优先选择。

实际示例

让我们考虑一个具体的例子来说明这些规则的应用:

#include <iostream>

template<typename T>
void print(T x) {
    std::cout << "Generic template function\n";
}

void print(int x) {
    std::cout << "Non-template function\n";
}

template<typename T, typename... Args>
void print(T x, Args... args) {
    std::cout << "Template function with parameter pack\n";
}

int main() {
    print(10);  // Selects the non-template function
    print(10, 20);  // Selects the template function with parameter pack
}

在这个示例中,当我们调用 print(10) 时,选择的是非模板函数 void print(int x),因为它是一个非模板函数,即使 template<typename T> void print(T x) 也能匹配。当我们调用 print(10, 20) 时,选择的是带有参数包的模板函数 template<typename T, typename... Args> void print(T x, Args... args),因为它可以接受多个参数。

这些规则确保了函数调用的一致性和可预测性,同时也提供了足够的灵活性来处理不同类型和数量的参数。

1.7 改善完美匹配

在 C++ 中,当我们讨论“完美匹配”(perfect match)时,通常指的是编译器如何选择最合适的函数重载来处理特定类型的参数。在 C++11 引入了右值引用后,这种匹配变得更加灵活。

你提到的情况是关于函数 report 的重载。在 C++11 之前,为了处理左值和右值的不同情况,通常需要定义两个重载版本的函数,一个接受非 const 引用,另一个接受 const 引用。这样做的原因是:

  • const 引用只能绑定到非 const 左值。
  • const 引用可以绑定到任何类型的左值或右值。

这里有一个例子来说明你的描述:

#include <iostream>

void report(int&);      // #1 - 非const引用
void report(int const&); // #2 - const引用

int main() {
    int k = 0;
    for (k = 0; k < 10; ++k) {
        report(k);       // 调用 #1,因为 k 是非const左值
    }
    report(42);          // 调用 #2,因为 42 是右值,只能与 const 引用匹配
    return 0;
}

在这个例子中:

  • k 作为参数传递给 report 时,它是一个非 const 左值,因此匹配到第一个重载版本 report(int&)
  • 42 作为参数传递给 report 时,它是一个右值,只能与第二个重载版本 report(int const&) 匹配。

然而,在 C++11 中引入了移动语义和右值引用,这允许我们更优雅地处理右值。例如,我们可以使用统一的接口来处理左值和右值:

#include <iostream>

void report(int&);           // #1 - 非const引用
void report(int&&);          // #3 - 右值引用

int main() {
    int k = 0;
    for (k = 0; k < 10; ++k) {
        report(k);           // 调用 #1
    }
    report(42);              // 调用 #3
    return 0;
}

在这个例子中:

  • report(int&) 仍然用于处理非 const 左值。
  • 新增的 report(int&&) 用于处理右值。

这种情况下,我们不再需要 report(int const&) 这个重载版本,因为右值会直接匹配到 report(int&&)。同时,对于非 const 左值,report(int&) 就足够了。这种方式被称为“完美转发”(perfect forwarding),并且是 C++11 标准中的一个关键特性。


在 C++11 中,引入了右值引用以及 std::move,这让编写泛型代码变得更加容易,同时也支持了移动语义。你的示例展示了如何通过 std::forwardstd::move 来实现完美转发,并且正确地选择了重载版本。

下面是你的示例代码的完整版本,我将对其进行注释以帮助理解:

#include <iostream>

struct Value {};  // 假设 Value 是一个简单的结构体

// 重载 pass 函数
void pass(Value const& v) {  // #1 - 接受 const 引用
    std::cout << "pass(const&)\n";
}

void pass(Value&& v) {       // #2 - 接受右值引用
    std::cout << "pass(rvalue&)\n";
}

void g(Value&& x) {
    // x 是一个左值引用到右值
    pass(x);                 // 调用 #1,因为 x 是一个左值
    pass(Value());           // 调用 #2,因为 Value() 是一个纯右值 (prvalue)
    pass(std::move(x));      // 调用 #2,因为 std::move(x) 是一个将亡值 (xvalue)
}

int main() {
    g(Value());              // 创建一个临时 Value 对象并传递给 g
    return 0;
}

这里的关键点在于:

  1. g(Value()) 创建了一个临时 Value 对象,并将其作为右值传递给了 g。在 g 内部,x 是一个绑定到该右值的左值引用。

  2. pass(x); 在这里,x 是一个左值,因此它与 pass(Value const&) 完全匹配,即调用 #1。

  3. pass(Value()); 这里创建了一个新的临时 Value 对象,并立即作为纯右值 (prvalue) 传递给 pass。因此,它与 pass(Value&&) 完全匹配,即调用 #2。

  4. pass(std::move(x)); 使用 std::movex 转换为一个将亡值 (xvalue),这使得它可以与 pass(Value&&) 完全匹配,即调用 #2。

总结来说,std::move 可以将一个左值转换为一个将亡值 (xvalue),这使得它可以匹配到 Value&& 类型的参数。而 Value() 创建的临时对象本身就是纯右值 (prvalue),可以直接匹配到 Value&&


你的描述涉及到成员函数调用时的完美匹配问题,特别是当调用成员函数时如何选择正确的重载版本。在 C++ 中,成员函数的调用也会涉及完美匹配的概念,尤其是当对象是左值或右值时。

下面是一个根据你的描述的例子,展示如何选择成员函数的重载版本:

#include <iostream>

class Wonder {
public:
    void tick();             // #1 - 非const成员函数
    void tick() const;       // #2 - const成员函数
    void tack() const;       // #3 - const成员函数
};

void Wonder::tick() {
    std::cout << "tick() called\n";
}

void Wonder::tick() const {
    std::cout << "tick() const called\n";
}

void Wonder::tack() const {
    std::cout << "tack() const called\n";
}

void run(Wonder& device) {
    device.tick();           // 调用 #1,因为 device 是非const左值
    device.tack();           // 调用 #3,因为没有非const版本的 tack()
}

int main() {
    Wonder w;
    run(w);
    return 0;
}

这里的关键点是:

  1. device.tick(); 在这里,device 是一个非 const 的左值引用,所以它与 tick() 的非 const 版本 (#1) 完全匹配。

  2. device.tack(); 由于没有非 const 版本的 tack() 成员函数,所以它会与 tack()const 版本 (#3) 完全匹配。

总结一下,成员函数的选择遵循以下规则:

  • 如果成员函数被调用的对象是一个非 const 左值,则优先选择非 const 的重载版本。
  • 如果成员函数被调用的对象是一个 const 左值或右值,则选择 const 的重载版本。
  • 如果对象是右值,那么只有 const 的重载版本能够匹配,因为在 C++ 中右值总是被视为 const

在你的例子中,run 函数接收一个非 constWonder 对象引用,并调用其成员函数。由于 tack() 没有非 const 的版本,所以它总是调用 const 版本的 tack()


你提到的情况确实会产生函数重载选择的歧义。在 C++ 中,如果多个函数重载版本都与提供的参数完美匹配,那么编译器无法确定应该选择哪一个版本,这会导致编译错误。让我们来看一下你给出的例子:

#include <iostream>

void report(int);      // #1 - 接受 int 参数
void report(int&);     // #2 - 接受 int& 参数
void report(int const&);// #3 - 接受 int const& 参数

int main() {
    int k = 0;
    for (k = 0; k < 10; ++k) {
        report(k);     // 编译错误:#1 和 #2 都是完美匹配
    }
    report(42);        // 编译错误:#1 和 #3 都是完美匹配
    return 0;
}

在这个例子中,report 函数有三个重载版本:

  1. void report(int); - 接受 int 类型的参数。
  2. void report(int&); - 接受 int 类型的引用。
  3. void report(int const&); - 接受 int 类型的 const 引用。

当你尝试调用这些函数时,会发生以下情况:

  1. report(k); - 这里 k 是一个非 const 左值,所以它可以与 report(int&) 完美匹配,也可以与 report(int) 完美匹配。因此,编译器无法决定使用哪个版本。

  2. report(42); - 这里 42 是一个右值,它可以与 report(int const&) 完美匹配,也可以与 report(int) 完美匹配。同样地,编译器无法决定使用哪个版本。

在 C++ 中,当存在多个候选函数,并且它们都被认为是“最佳匹配”,但没有一个明显优于其他的情况下,编译器会报告一个错误,指出函数选择的歧义。

为了避免这种情况,你可以采取以下几种方法之一:

  1. 删除冗余的重载:只保留一个版本,比如 void report(int const&) 或者 void report(int&),根据你的需求来决定。
  2. 明确指定类型转换:如果你确实需要这些重载,并且想要在某些情况下明确指定使用哪一个版本,你可以通过显式类型转换来避免歧义。

例如,你可以修改 main 函数来消除歧义:

#include <iostream>

void report(int);      // #1 - 接受 int 参数
void report(int&);     // #2 - 接受 int& 参数
void report(int const&);// #3 - 接受 int const& 参数

int main() {
    int k = 0;
    for (k = 0; k < 10; ++k) {
        report(static_cast<int&>(k)); // 明确选择 #2
    }
    report(static_cast<int const&>(42)); // 明确选择 #3
    return 0;
}

这里使用 static_cast 来明确指定类型转换,从而消除了编译时的歧义。

如果你不需要这些重载版本中的某个,最好还是删除它以简化代码并避免潜在的歧义。


2.重载的细节

这段文字描述的是C++中的重载解析(overload resolution)规则,特别是当涉及到模板函数和非模板函数之间的选择时的情况。让我们逐步分析给出的例子和描述:

重载解析规则:

  1. 非模板优先原则:当一个非模板函数和一个模板函数在其他所有方面(参数列表等)完全一致时,编译器会优先选择非模板函数。这适用于无论是通过泛型模板定义生成的实例,还是作为显式特化提供的实例。
  2. 特化优先原则:如果需要在两个模板函数之间做出选择,并且其中一个实际上比另一个更特化,那么更特化的那个将被优先选择。
  3. 参数包特例:当两个模板除了一个添加了参数包(...)之外在其他方面都相同的情况下,没有参数包的模板被认为更特化。

给出的例子:

template<typename T>
int f(T); // #1 - 模板函数

void f(int); // #2 - 非模板函数

int main() {
    return f(7); // ERROR: selects #2, which doesn't return a value
}

在这个例子中,我们有两个函数:

  • f(T) 是一个模板函数,可以接受任何类型的参数。
  • f(int) 是一个非模板函数,只接受整数类型的参数。

当我们尝试调用 f(7) 时,编译器会尝试解析哪个版本的 f 应该被调用。由于 f(int) 是一个非模板函数并且其参数列表与给定的参数类型相匹配,所以它会被优先选择。但是,因为 f(int) 不返回任何值,而 main 函数试图返回一个 int 类型的值,所以这会导致一个编译错误。

16.2.4 节提到的内容:

这一节可能涉及的是成员函数重载解析时的一些特殊规则,尤其是当这些成员函数的参数列表涉及到了复制构造函数或移动构造函数的参数时。这里提到的是,当成员函数的参数列表与复制或移动构造函数的参数列表相似时,可能会出现一些意料之外的行为,这是因为重载解析规则会首先考虑参数列表的兼容性而不是函数的其他特性(如返回类型)。

4.1.2 节讨论的例子:

这部分可能是关于当两个模板函数除了一个模板添加了参数包以外在其他方面都相同时的处理方式。在这种情况下,没有参数包的模板被视为更特化,因此如果它能匹配调用,那么它将被优先选择。

总结来说,C++ 的重载解析规则非常细致且复杂,理解这些规则对于编写正确且高效的代码至关重要。

2.1 转换序列:

在C++中,当一个表达式被用于需要不同类型的上下文时,可能需要进行一系列的转换来使该表达式与期望的类型匹配。这些转换可以分为多个步骤,并且遵循一定的顺序和限制。

示例代码:

class Base {
public:
    operator short() const;
};

class Derived : public Base {
};

void count(int);
void count(short);

void process(Derived const& object) {
    count(object); // matches with user-defined conversion
}

解析过程:

当我们调用 count(object) 时,编译器需要确定哪个 count 函数是最合适的。在这里,objectDerived 类型的引用,而 count 函数分别接受 intshort 类型的参数。

为了使 objectcount 函数的参数类型匹配,我们需要进行一系列转换。这些转换可以分为几个步骤:

  1. 派生到基类的转换:从 Derived const&Base const& 的转换。这是一个标准转换,因为派生类的对象可以被隐式转换为其基类对象。这是 glvalue 转换,意味着它不会创建一个新的对象,而是保留原始对象的身份。

  2. 用户定义的转换Base 类中的 operator short() 成员函数允许将 Base const& 对象转换为 short 类型。这是用户定义的转换,因为它是由程序员明确定义的。

  3. 类型提升short 类型自动提升为 int 类型。这也是一个标准转换,因为在某些情况下,较小的整数类型会被提升为 int 类型。

最佳匹配函数的选择:

在本例中,count(int)count(short) 都是候选函数。根据上述转换序列,我们可以看到,为了使 count(int) 成为匹配项,需要进行三个步骤的转换。然而,对于 count(short),我们只需要进行两个步骤的转换:从 Derived const&Base const&,然后再从 Base const&short

根据C++的重载解析规则,如果一个函数匹配所需的转换序列比另一个函数短,则前者更优选。因此,在这种情况下,count(short) 会是更好的匹配,因为它不需要第三个步骤的类型提升转换。

重载解析的原则:

  • 最少转换原则:选择需要最少转换步骤的函数。
  • 转换序列的优先级:标准转换优于用户定义的转换,而用户定义的转换又优于构造函数转换。
  • 转换序列的组合:在转换序列中,最多只能有一个用户定义的转换。如果存在多个用户定义的转换,则它们必须是相互独立的。

总结起来,count(short) 将被选为 process 中调用 count(object) 的最佳匹配函数,因为它需要较少的转换步骤。


2.2 指针转换

在C++中,指针可以进行一些特殊的转换,这些转换被视为标准转换的一部分。这些转换包括:

  1. 转换为布尔类型:任何非空指针都会被转换为 true,而空指针会被转换为 false
  2. 转换为 void*:任何类型的指针都可以转换为 void*
  3. 派生类指针转换为基类指针:派生类的指针可以被隐式转换为其基类的指针。
  4. 成员指针的转换:指向派生类成员的指针可以被转换为指向基类相同成员的指针。

排名规则

在函数重载解析中,转换的排名很重要。例如,从指针到 bool 的转换被认为是比其他类型的标准转换更差的转换。这意味着如果一个函数接受 bool 参数,并且另一个函数接受 void* 参数,那么当传递一个指针给这两个函数时,后者将是更优的选择。

示例代码

void check(void*);
void check(bool);

void rearrange(Matrix* m) {
    check(m); // calls the void* version
}

分析

在这个例子中,rearrange 函数接收一个 Matrix* 类型的指针,并调用 check 函数。check 有两个版本:

  1. check(void*)
  2. check(bool)

当我们传递 Matrix*check 函数时,有两种可能的转换:

  • 转换为 void*:这是一个标准的指针转换,没有额外的操作。
  • 转换为 bool:这是一个更糟糕的转换,因为虽然指针可以被隐式转换为布尔值,但这并不是最理想的匹配,特别是在有更合适的转换可用的情况下。

根据C++的重载解析规则,当有多个候选函数时,会优先选择需要较少转换或者转换等级较高的函数。在这种情况下,由于从 Matrix*void* 的转换是标准转换,而到 bool 的转换则被视为较次的转换,因此 check(void*) 将被选为更优的函数,即:

void rearrange(Matrix* m) {
    check(m); // calls the void* version
}

因此,rearrange 函数中的 check(m) 调用将会匹配 check(void*) 版本,而不是 check(bool) 版本。这是因为从指针到 void* 的转换优于到 bool 的转换。


指针转换的类别

在C++中,指针可以进行一些特殊的转换,这些转换被视为标准转换的一部分。这些转换包括但不限于:

  1. 转换为 void* 类型
  2. 派生类指针转换为基类指针

指针转换的排名

在函数重载解析中,转换的排名很重要。例如,从指针到 void* 的转换被认为是比从派生类指针到基类指针的转换更糟糕的转换。这意味着如果一个函数接受 void* 参数,并且另一个函数接受基类指针参数,那么当传递一个派生类指针给这两个函数时,后者将是更优的选择。

示例代码

class Interface {};
class CommonProcesses : public Interface {};
class Machine : public CommonProcesses {};

char* serialize(Interface*); // #1
char* serialize(CommonProcesses*); // #2

void dump(Machine* machine) {
    char* buffer = serialize(machine); // calls #2
}

分析

在这个例子中,dump 函数接收一个 Machine* 类型的指针,并调用 serialize 函数。serialize 有两个版本:

  1. serialize(Interface*)
  2. serialize(CommonProcesses*)

当我们传递 Machine*serialize 函数时,有两种可能的转换:

  • 转换为 CommonProcesses*:这是一个派生类指针到基类指针的转换。
  • 转换为 Interface*:这也是一个派生类指针到基类指针的转换,但是转换到更远的基类。

根据C++的重载解析规则,当有多个候选函数时,会优先选择需要较少转换或者转换等级较高的函数。在这种情况下,从 Machine*CommonProcesses* 的转换优于到 Interface* 的转换,因为后者涉及更远的基类转换。因此,serialize(machine) 将匹配 serialize(CommonProcesses*) 版本,即:

void dump(Machine* machine) {
    char* buffer = serialize(machine); // calls #2
}

成员指针的转换

对于成员指针,也有类似的规则。如果存在两种相关的指针成员类型的转换,优先选择继承图中“最接近的基”(即派生最少的基)的转换。这意味着如果成员指针可以被转换为指向不同基类成员的指针,那么优先选择转换到继承层次结构中更接近的基类成员的指针。

总结

C++中的指针转换规则考虑了继承关系和转换的质量。当涉及到继承层次结构中的指针转换时,更接近基类的转换被优先考虑。这些规则有助于确保代码的可读性和可维护性,同时也保证了类型安全。


2.3 初始化列表

这段描述涉及C++中的初始化列表(initializer list)及其如何用于初始化不同类型的数据结构。让我们逐步解析这段描述并理解其中的概念。

初始化列表

初始化列表是一种特殊的容器类型 std::initializer_list<T>,它可以用来初始化一组同类型的元素。初始化列表可以被直接传递给函数,也可以用于构造其他类型的数据结构。

示例代码

#include <iostream>
#include <initializer_list>
#include <string>
#include <vector>
#include <complex>

void f(std::initializer_list<int>) {
    std::cout << "#1\n";
}

void f(std::initializer_list<std::string>) {
    std::cout << "#2\n";
}

void g(const std::vector<int>& vec) {
    std::cout << "#3\n";
}

void h(const std::complex<double>& cmplx) {
    std::cout << "#4\n";
}

struct Point {
    int x, y;
};

void i(const Point& pt) {
    std::cout << "#5\n";
}

int main() {
    f({1, 2, 3});          // prints #1
    f({"hello", "initializer", "list"}); // prints #2
    g({1, 1, 2, 3, 5});     // prints #3
    h({1.5, 2.5});          // prints #4
    i({1, 2});              // prints #5
}

分析

在这个例子中,main 函数展示了初始化列表的不同用途:

  1. 初始化列表到 std::initializer_list 的转换

    • f({1, 2, 3}):这里,初始化列表 {1, 2, 3} 被直接转换为 std::initializer_list<int>,并被传递给第一个 f 函数。
  2. 初始化列表到 std::string 的转换

    • f({"hello", "initializer", "list"}):这里,初始化列表中的每个字符串字面量都被转换为 std::string 类型,并构成 std::initializer_list<std::string>,然后传递给第二个 f 函数。
  3. 初始化列表到 std::vector 的转换

    • g({1, 1, 2, 3, 5}):这里,初始化列表被用于构造 std::vector<int>,然后传递给 g 函数。这是通过调用 std::vector 的构造函数实现的,该构造函数接受 std::initializer_list<int>
  4. 初始化列表到 std::complex 的转换

    • h({1.5, 2.5}):这里,初始化列表中的两个元素被用于构造 std::complex<double>,这相当于 std::complex<double>(1.5, 2.5)
  5. 初始化列表到聚合类型的转换

    • i({1, 2}):这里,初始化列表 {1, 2} 用于初始化 Point 结构体的成员变量 xy。由于 Point 是聚合类型,因此可以直接使用初始化列表进行初始化。

聚合初始化

聚合初始化适用于满足以下条件的类型:

  • 没有用户提供的构造函数。
  • 没有私有或受保护的非静态数据成员。
  • 没有基类。
  • 没有虚函数。
  • 在 C++14 之前,不能有默认的成员初始化。
  • 在 C++17 及以后,可以使用公共基类。

聚合初始化允许使用初始化列表直接初始化类的成员,无需显式调用构造函数。

总结

初始化列表在C++中是一种非常强大的工具,可以用于多种目的,包括初始化 std::initializer_list、构造其他类型的数据结构,以及聚合初始化。了解这些功能可以帮助编写更简洁、更高效的代码。


这段描述涉及C++中初始化列表的重载解析机制,特别是当初始化列表用于初始化不同类型的对象时。让我们逐步解析这段描述并理解其中的概念。

初始化列表的重载解析

在C++中,当使用初始化列表初始化对象时,重载解析分为两个阶段进行:

  1. 第一阶段:只考虑初始化列表构造函数。对于某些类型 T,其唯一非默认参数是 std::initializer_list<T> 类型的构造函数(删除顶层引用和 const/volatile 限定符之后)。

  2. 第二阶段:如果没有找到这样的可行构造函数,接下来会考虑其他构造函数。

示例代码

#include <iostream>
#include <initializer_list>
#include <string>

template<typename T>
struct Array {
    Array(std::initializer_list<T>) {
        std::cout << "#1\n";
    }
    Array(unsigned n, T const&) {
        std::cout << "#2\n";
    }
};

void arr1(Array<int>);
void arr2(Array<std::string>);

int main() {
    arr1({1, 2, 3, 4, 5}); // prints #1
    arr1({1, 2});          // prints #1
    arr1({10u, 5});        // prints #2
    arr2({"hello", "initializer", "list"}); // prints #1
    arr2({10, "hello"});   // prints #2
}

分析

在这个例子中,main 函数展示了初始化列表的不同用途:

  1. 初始化列表到 std::initializer_list 的转换

    • arr1({1, 2, 3, 4, 5}):这里,初始化列表 {1, 2, 3, 4, 5} 被直接转换为 std::initializer_list<int>,并通过调用 Array<int> 的初始化列表构造函数初始化 Array<int> 实例。
    • arr1({1, 2}):同样,初始化列表 {1, 2} 被转换为 std::initializer_list<int> 并用于初始化 Array<int> 实例。
  2. 初始化列表到 unsignedT const& 的转换

    • arr1({10u, 5}):这里,初始化列表 {10u, 5} 被用于调用 Array<int> 的非初始化列表构造函数,因为该构造函数接受一个 unsigned 和一个 int const& 参数。初始化列表中的第一个元素被转换为 unsigned,而第二个元素已经是一个 int 类型。
  3. 初始化列表到 std::string 的转换

    • arr2({"hello", "initializer", "list"}):这里,初始化列表中的每个字符串字面量都被转换为 std::string 类型,并通过调用 Array<std::string> 的初始化列表构造函数初始化 Array<std::string> 实例。
  4. 初始化列表到 unsignedT const& 的转换

    • arr2({10, "hello"}):这里,初始化列表 {10, "hello"} 被用于调用 Array<std::string> 的非初始化列表构造函数,因为该构造函数接受一个 unsigned 和一个 std::string const& 参数。初始化列表中的第一个元素被转换为 unsigned,而第二个元素已经被转换为 std::string 类型。

重载解析的细节

  • 当使用初始化列表初始化 Array<int> 对象时,初始化列表构造函数总是比非初始化列表构造函数更匹配,因此 arr1 的前两次调用都使用了初始化列表构造函数。
  • 对于 Array<std::string>,当初始化列表构造函数不可用时(例如,当初始化列表包含一个 unsigned 和一个 std::string 时),会调用非初始化列表构造函数,如 arr2 的第二次调用所示。

总结

初始化列表在C++中是一种非常灵活的工具,可以用于多种目的,包括初始化 std::initializer_list、构造其他类型的数据结构,以及聚合初始化。了解这些功能和重载解析的规则可以帮助编写更简洁、更高效的代码。


这段描述涉及C++中的函子(functor)和代理函数(proxy function)的概念,特别是在重载解析中的作用。让我们逐步解析这段描述并理解其中的概念。

2.4 函子

函子(functor)是一种类类型,它可以像函数一样被调用。函子通常包含一个函数调用操作符 operator(),这使得可以使用圆括号语法来调用它们,就像调用普通函数一样。

代理函数

代理函数是在某些情况下由编译器自动生成的一种虚拟函数,它出现在重载解析的过程中。当一个类对象具有指向函数类型指针(或指向函数类型引用)的隐式转换操作符时,编译器会生成一个代理函数来代表这个转换操作符的目标类型。

示例代码

using FuncType = void (*)(double, int);

class IndirectFunctor {
public:
    void operator()(double, double) const;
    operator FuncType*() const;
};

void activate(IndirectFunctor const& funcobj) {
    funcObj(3, 5); // ERROR: ambiguous
}

分析

在这个例子中,IndirectFunctor 类包含了一个函数调用操作符 operator() 和一个隐式转换操作符 operator FuncType*()activate 函数接收一个 IndirectFunctor 的常量引用,并尝试调用 funcObj(3, 5)

重载解析的过程

  1. 成员函数操作符:首先,funcObj 的成员函数操作符 operator() 被添加到重载集合中。该操作符接受两个 double 参数。

  2. 代理函数:其次,由于 IndirectFunctor 类有一个隐式转换操作符 operator FuncType*(),编译器会生成一个代理函数。该代理函数具有与 FuncType 相对应的参数类型,即 doubleint

重载解析的细节

当我们调用 funcObj(3, 5) 时,编译器需要解析哪个函数是最合适的。这里有两个候选函数:

  1. 成员函数操作符funcObj.operator()(double, double) const,它接受一个 IndirectFunctor const& 和两个 double 参数。
  2. 代理函数:这个代理函数具有 FuncType*doubleint 的参数类型。

重载解析的结果

  • 成员函数操作符:与 funcObj 的匹配是完美的,因为它是一个 IndirectFunctor const&
  • 代理函数:与 funcObj 的匹配较差,因为它需要通过用户定义的转换操作符 operator FuncType*() 来转换。

然而,代理函数与 35 的匹配较好,因为它们分别是 doubleint 类型,与代理函数的参数类型相符。

由于成员函数操作符和代理函数都与 35 匹配得较好,而与 funcObj 的匹配程度不同,因此编译器无法确定哪一个函数是最合适的,从而导致了重载解析的歧义。

总结

代理函数是C++中较为复杂的概念之一,通常在涉及函子和重载解析时出现。了解这些概念有助于避免潜在的编译错误和理解更复杂的代码片段。在实际编程中,函子通常用于模拟函数行为,而代理函数则是编译器自动生成的,用于处理特定类型的转换操作符。


2.5 C++中重载解析的一些其他应用场景,

这段描述涵盖了C++中重载解析的一些其他应用场景,特别是涉及到函数地址获取和对象初始化的情况。我们来分析一下提供的示例代码:

int numElems(Matrix const&);
int numElems(Vector const&);

int (*funcPtr)(Vector const&) = numElems; // selects #2

在这个例子中,numElems 是一个重载集,包含两个同名函数。当我们尝试将 numElems 赋值给 funcPtr 时,编译器需要解析出应该使用哪一个版本的 numElems 函数。在这种情况下,由于 funcPtr 的类型是 int (*)(Vector const&),编译器会选择第二个版本的 numElems 函数,即 int numElems(Vector const&);,因为它的参数类型与 funcPtr 的目标类型一致。

接下来是关于初始化的例子:

#include <string>

class BigNum {
public:
    BigNum(long n);          // #1
    BigNum(double n);        // #2
    BigNum(std::string const&); // #3
    operator double();       // #4
    operator long();         // #5
};

void initDemo() {
    BigNum bn1(100103);      // selects #1
    BigNum bn2("7057103224.095764"); // selects #3
    long ln = bn1;           // selects #5
}

在这个例子中,BigNum 类包含了多个构造函数和转换操作符。当创建 BigNum 对象时,编译器需要决定使用哪一个构造函数或转换操作符来进行初始化。

  1. 初始化 bn1

    • bn1 使用整数 100103 初始化,这与构造函数 BigNum(long n) 的签名匹配,因此选择构造函数 #1。
  2. 初始化 bn2

    • bn2 使用字符串 "7057103224.095764" 初始化,这与构造函数 BigNum(std::string const&) 的签名匹配,因此选择构造函数 #3。
  3. 初始化 ln

    • ln 是一个 long 类型的变量,被初始化为 bn1 的值。这意味着需要一个从 BigNumlong 的转换。编译器会选择转换操作符 operator long(),即 #5。

重载解析的规则

重载解析的规则非常复杂,但总的来说,编译器会试图找到最合适的函数或者构造函数来进行调用。这涉及到对参数类型、转换操作符以及用户定义的转换的比较。通常,如果存在一个完全匹配的函数,则优先选择它。如果没有直接匹配的函数,编译器会寻找可以通过最少转换达到匹配的函数。

初始化的细节

对于初始化来说,还有一些额外的规则需要考虑:

  • 最匹配的构造函数:如果存在多个构造函数,编译器会选择与提供的实参最匹配的一个。
  • 用户定义的转换:如果类提供了用户定义的转换操作符,这些操作符可以被用来实现从类类型到基本类型或其他类型的转换。
  • 构造函数的可访问性:只有可访问的构造函数才会被考虑。

总结

重载解析不仅在函数调用时起作用,还会影响到函数地址的获取以及对象的初始化。在初始化的情况下,选择合适的构造函数或转换操作符对于确保正确的类型转换至关重要。这些规则虽然通常会产生直观的结果,但在某些情况下可能会产生意料之外的行为,尤其是在处理复杂的用户定义转换和构造函数重载时。


补充例子

当然,下面是一个完整的、可编译运行的 C++ 示例代码,它演示了如何重载函数模板以及如何处理不同类型的参数。这个示例将包括你提到的所有情况:

#include <iostream>

// Non-template version of max function
int max(int a, int b) {
    return b < a ? a : b;
}

// Template version of max function
template<typename T>
T max(T a, T b) {
    return b < a ? a : b;
}

int main() {
    // Calls the non-template version for two ints
    std::cout << "max(7, 42): " << max(7, 42) << std::endl;

    // Calls max<double>(by argument deduction)
    std::cout << "max(7.0, 42.0): " << max(7.0, 42.0) << std::endl;

    // Calls max<char>(by argument deduction)
    std::cout << "max('a','b'): " << max('a','b') << std::endl;

    // Calls max<int>(by argument deduction)
    std::cout << "max<>(7, 42): " << max<>(7, 42) << std::endl;

    // Calls max<double>(no argument deduction)
    std::cout << "max<double>(7, 42): " << max<double>(7, 42) << std::endl;

    // Calls the non-template version because of different types ('a' and 42.7)
    std::cout << "max('a', 42.7): " << max('a', 42.7) << std::endl;

    return 0;
}

这段代码说明和示例展示了在C++中如何重载函数模板以及如何处理不同类型的参数。让我们逐行分析这段代码说明中的关键点,并解释其行为:

  1. Non-template vs. Template Overloading:

    • 当存在一个非模板函数和一个模板函数具有相同的名称时,如果非模板函数能够直接接受传入的参数类型,那么优先考虑非模板函数。
    • 如果非模板函数不能直接接受这些参数(即需要类型转换),则会考虑是否可以通过模板参数的类型推导来创建一个更匹配的模板实例。
  2. Function Calls:

    • max(7, 42); 调用的是非模板版本的 max 函数,因为它可以直接接受两个 int 类型的参数。
    • max(7.0, 42.0); 调用的是模板版本的 max 函数,通过参数推导确定模板参数为 double
    • max('a','b'); 同样调用模板版本的 max 函数,参数推导确定模板参数为 char
    • max<>(7, 42); 显式地指定了这是一个模板函数调用,参数推导确定模板参数为 int
    • max<double>(7, 42); 指定了模板参数为 double,但是实际上传递的参数类型为 int,因此这个调用不会利用模板参数,而是将 int 参数提升到 double
    • max('a', 42.7); 这个调用最终选择了非模板版本的 max 函数,因为模板版本无法直接接受这两种不同类型的参数(chardouble)而不需要额外的类型转换。在这种情况下,非模板版本允许将 chardouble 都转换为 int
  3. Template Argument Deduction:

    • 当调用模板函数时,如果没有显式指定模板参数,编译器会尝试从实际的参数类型推导出模板参数的类型。
    • 如果推导过程导致需要复杂的类型转换,那么非模板函数可能会被优先考虑。

基于上述解释,我们可以总结出几个关键点:

  • 非模板函数对于简单的类型匹配有优先权。
  • 模板函数在需要类型推导并且能更好地匹配参数类型时会被选择。
  • 显式指定模板参数列表可以控制模板实例的选择。
  • 自动类型转换仅适用于非模板函数,在模板函数的参数推导过程中不会发生自动类型转换。

请注意,代码片段中的行号和注释符号(如 /H313)看起来像是误打或者是文档的排版标记,它们不是标准C++语法的一部分。此外,代码片段缺少了完整的上下文(例如包含语句、命名空间定义等),因此它不是一个可直接编译运行的完整C++程序。如果你需要一个具体的示例,我可以帮你构建一个完整的可编译的C++示例代码。

这个示例展示了如何重载函数模板以允许显式指定返回类型。下面是根据你的描述重构的一个完整的示例代码:

#include <iostream>

// 第一个函数模板,返回类型由编译器推导
template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype((b < a) ? a : b) {
    return (b < a) ? a : b;
}

// 第二个函数模板,允许显式指定返回类型
template<typename RT, typename T1, typename T2>
RT max(T1 a, T2 b) {
    return (b < a) ? a : b;
}

int main() {
    // 使用第一个函数模板,返回类型由编译器推导
    auto a = ::max(4, 7.2);
    std::cout << "max(4, 7.2): " << a << std::endl;

    // 使用第二个函数模板,显式指定返回类型为 long double
    auto b = ::max<long double>(7.2, 4);
    std::cout << "max<long double>(7.2, 4): " << b << std::endl;

    // 尝试使用第二个函数模板,但是两个函数模板都可以匹配
    // 这将导致编译错误,因为编译器无法确定使用哪个模板
    // auto c = ::max<int>(4, 7.2);  // Uncommenting this line will cause a compilation error

    return 0;
}

解析:

  1. 第一个函数模板:

    • 定义了一个函数模板 max,它接受两个参数 ab,这两个参数可以是任意类型。
    • 使用 decltype 来指定返回类型,这样编译器可以根据比较操作的结果推导出返回类型。
  2. 第二个函数模板:

    • 允许用户显式指定返回类型 RT
    • 接受两个参数 ab,这两个参数可以是任意类型。
    • 返回类型为 RT
  3. 函数调用:

    • auto a = ::max(4, 7.2); - 编译器推导出返回类型为 double,因为 7.2double 类型。
    • auto b = ::max<long double>(7.2, 4); - 显式指定返回类型为 long double,即使其中一个参数是 double
    • auto c = ::max<int>(4, 7.2); - 这一行如果取消注释将会导致编译错误,因为两个模板都可以匹配,编译器无法确定应该使用哪一个。

关于编译错误的解释:

当你尝试调用 max<int>(4, 7.2); 时,编译器发现有两个可能的匹配函数模板:

  • 第一个模板可以接受 47.2 并推导出返回类型为 double
  • 第二个模板可以接受 47.2 并显式指定返回类型为 int

由于这两个模板都提供了一个有效的匹配,编译器无法确定应该使用哪一个。这将导致一个编译错误,提示“ambiguous call”(模棱两可的调用)。

要解决这个问题,你需要确保两个函数模板之间存在明确的优先级关系或者更改其中一个模板的签名以避免冲突。例如,你可以改变第一个模板的签名以避免与第二个模板产生冲突,或者为第二个模板添加额外的条件以限制其适用范围。

这段代码示例展示了如何重载函数模板以处理不同类型的数据。以下是根据你的描述重构的一个完整的示例代码:

#include <iostream>
#include <cstring>
#include <string>

// 任意类型的两个最大值
template<typename T>
T max(T a, T b) {
    return b < a ? a : b;
}

// 两个指针内容的最大值
template<typename T>
T* max(T* a, T* b) {
    return *b < *a ? a : b;
}

// 两个 C 字符串的最大值
char const* max(char const* a, char const* b) {
    return std::strcmp(b, a) < 0 ? a : b;
}

int main() {
    int a = 7;
    int b = 42;
    auto m1 = ::max(a, b);  // max(): 两个 int 类型的值
    std::cout << "Max of int values: " << m1 << std::endl;

    std::string s1 = "hey";
    std::string s2 = "you";
    auto m2 = ::max(s1, s2);  // max(): 两个 std::string 类型的值
    std::cout << "Max of string values: " << m2 << std::endl;

    int* p1 = &b;
    int* p2 = &a;
    auto m3 = ::max(p1, p2);  // max(): 两个指针类型的值
    std::cout << "Max of pointer values: " << *m3 << std::endl;

    char const* x = "hello";
    char const* y = "world";
    auto m4 = ::max(x, y);  // max(): 两个 C 字符串类型的值
    std::cout << "Max of C string values: " << m4 << std::endl;

    return 0;
}

解析:

  1. 任意类型的两个最大值:

    • 定义了一个函数模板 max,它接受两个同类型的参数 ab,并返回较大的一个。
  2. 两个指针内容的最大值:

    • 定义了一个函数模板 max,它接受两个指向同类型数据的指针 ab,并返回指向较大值的指针。
  3. 两个 C 字符串的最大值:

    • 定义了一个非模板函数 max,它接受两个 C 字符串指针 ab,并返回字典序较大的一个。
  4. 函数调用:

    • auto m1 = ::max(a, b); - 调用模板版本的 max 函数,用于比较两个 int 类型的值。
    • auto m2 = ::max(s1, s2); - 调用模板版本的 max 函数,用于比较两个 std::string 类型的值。
    • auto m3 = ::max(p1, p2); - 调用指针版本的模板 max 函数,用于比较两个指向 int 的指针的内容。
    • auto m4 = ::max(x, y); - 调用非模板版本的 max 函数,用于比较两个 C 字符串。

关于重载解析的注意事项:

  • 在这个示例中,我们定义了三个不同的 max 函数(一个模板和两个非模板),每个函数都有不同的用途。
  • 对于 int 类型的值,编译器会选择模板版本的 max 函数。
  • 对于 std::string 类型的值,编译器同样会选择模板版本的 max 函数。
  • 对于指向 int 的指针,编译器会选择指针版本的模板 max 函数。
  • 对于 C 字符串,编译器会选择非模板版本的 max 函数。

在这个示例中,重载解析是明确的,因为每个函数调用都能唯一地匹配到一个函数实现。如果有任何歧义,你需要调整函数签名或添加额外的模板或非模板函数以消除歧义。

这段代码示例展示了如何重载函数模板以处理不同类型的数据,并通过引用传递参数。下面是根据你的描述重构的一个完整的示例代码,以及对其潜在问题的解释:

#include <iostream>
#include <cstring>

// 任意类型的两个最大值(引用调用)
template<typename T>
T const& max(T const& a, T const& b) {
    return b < a ? a : b;
}

// 最多两个 C 字符串(值调用)
char const* max(char const* a, char const* b) {
    return std::strcmp(b, a) < 0 ? a : b;
}

// 任何类型的最多三个值(引用调用)
template<typename T>
T const& max(T const& a, T const& b, T const& c) {
    return max(max(a, b), c);  // 注意这里的递归调用
}

int main() {
    auto m1 = ::max(7, 42, 68);  // OK
    std::cout << "Max of three int values: " << m1 << std::endl;

    char const* s1 = "frederic";
    char const* s2 = "anica";
    char const* s3 = "lucas";

    // 这里将导致运行时错误(未定义的行为)
    auto m2 = ::max(s1, s2, s3);

    std::cout << "Max of three C string values: " << m2 << std::endl;

    return 0;
}

解析:

  1. 任意类型的两个最大值:

    • 定义了一个函数模板 max,它接受两个同类型的引用参数 ab,并返回较大的一个的引用。
  2. 最多两个 C 字符串(值调用):

    • 定义了一个非模板函数 max,它接受两个 C 字符串指针 ab,并返回字典序较大的一个。
  3. 任何类型的最多三个值(引用调用):

    • 定义了一个函数模板 max,它接受三个同类型的引用参数 abc,并返回较大的一个的引用。这里使用了递归调用来找到三个值中的最大值。
  4. 函数调用:

    • auto m1 = ::max(7, 42, 68); - 调用模板版本的 max 函数,用于比较三个 int 类型的值。
    • auto m2 = ::max(s1, s2, s3); - 这里将导致运行时错误(未定义的行为)。

关于潜在问题的解释:

  • 当使用 max(s1, s2, s3) 调用时,内部的 max(max(a, b), c) 会导致问题,特别是当 ab 是 C 字符串时。
  • max(a, b) 的调用将创建一个新的临时 C 字符串(因为 ab 是值传递的),并通过引用返回。
  • 当这个临时 C 字符串作为 max 的返回值时,它将被销毁(因为它是一个临时对象),留下一个悬空的引用。
  • max(max(a, b), c) 被调用时,它将尝试使用这个已销毁的临时 C 字符串,导致未定义的行为。

解决方案:

为了避免这个问题,你可以修改 max 函数模板以避免使用 C 字符串的值传递,或者在递归调用时显式地处理 C 字符串的情况。例如,你可以为 C 字符串专门写一个非模板版本的 max 函数,而不是通过值传递来比较它们。

这里有一个修改后的示例,它避免了上述问题:

#include <iostream>
#include <cstring>

// 任意类型的两个最大值(引用调用)
template<typename T>
T const& max(T const& a, T const& b) {
    return b < a ? a : b;
}

// 最多两个 C 字符串(值调用)
char const* max(char const* a, char const* b) {
    return std::strcmp(b, a) < 0 ? a : b;
}

// 任何类型的最多三个值(引用调用)
template<typename T>
T const& max(T const& a, T const& b, T const& c) {
    if constexpr (std::is_same<T, char const*>::value) {
        return max(a, b, c);  // 使用非模板版本处理 C 字符串
    } else {
        return max(max(a, b), c);  // 使用模板版本处理其他类型
    }
}

int main() {
    auto m1 = ::max(7, 42, 68);  // OK
    std::cout << "Max of three int values: " << m1 << std::endl;

    char const* s1 = "frederic";
    char const* s2 = "anica";
    char const* s3 = "lucas";

    // 使用非模板版本处理 C 字符串
    auto m2 = ::max(s1, s2, s3);

    std::cout << "Max of three C string values: " << m2 << std::endl;

    return 0;
}

在这个修改后的示例中,我们检查了类型是否为 char const*,如果是,则使用非模板版本的 max 函数来处理 C 字符串,从而避免了创建临时 C 字符串的问题。

这段代码示例展示了重载解析规则可能导致的行为与预期不同的情况。下面是根据你的描述重构的一个完整的示例代码,以及对其潜在问题的解释:

#include <iostream>

// 两个任意类型的最大值
template<typename T>
T max(T a, T b) {
    std::cout << "max<T>()\n";
    return b < a ? a : b;
}

// 三个任意类型的最大值
template<typename T>
T max(T a, T b, T c) {
    return max(max(a, b), c);  // 注意这里使用了模板版本
}

int main() {
    ::max(47, 11, 33);  // oops: 这里使用的是 max<T>(), 而非 max(int,int)

    return 0;
}

解析:

  1. 两个任意类型的最大值:

    • 定义了一个函数模板 max,它接受两个同类型的参数 ab,并返回较大的一个。
  2. 三个任意类型的最大值:

    • 定义了一个函数模板 max,它接受三个同类型的参数 abc,并返回较大的一个。这里使用了递归调用来找到三个值中的最大值。
  3. 函数调用:

    • ::max(47, 11, 33); - 这里将导致使用模板版本的 max 函数,而非预期的非模板版本。

关于潜在问题的解释:

  • 当调用 max(47, 11, 33) 时,编译器将选择三个参数的模板版本。
  • 因为非模板版本的 max 函数(用于两个 int 类型的值)没有在之前声明,所以在调用 max(47, 11, 33) 时,编译器不知道存在这样的非模板版本。
  • 结果,即使你希望使用非模板版本的 max 函数来处理两个 int 类型的值,但由于声明顺序的原因,编译器会选择模板版本的 max 函数。

解决方案:

为了避免这个问题,你需要确保所有的重载版本都在使用前被声明。这里是修改后的代码,确保了所有的重载版本都在使用前被声明:

#include <iostream>

// 两个任意类型的最大值
template<typename T>
T max(T a, T b) {
    std::cout << "max<T>()\n";
    return b < a ? a : b;
}

// 两个 int 值的最大值
int max(int a, int b) {
    std::cout << "max(int,int)\n";
    return b < a ? a : b;
}

// 三个任意类型的最大值
template<typename T>
T max(T a, T b, T c) {
    return max(max(a, b), c);  // 注意这里使用了模板版本
}

int main() {
    ::max(47, 11, 33);  // 使用的是 max<T>(), 而非 max(int,int)

    return 0;
}

修改后的解析:

  1. 两个任意类型的最大值:

    • 保留原样。
  2. 两个 int 值的最大值:

    • 添加了一个非模板版本的 max 函数,它接受两个 int 类型的参数 ab,并返回较大的一个。
  3. 三个任意类型的最大值:

    • 保留原样。
  4. 函数调用:

    • ::max(47, 11, 33); - 这里仍然使用模板版本的 max 函数,因为 max(47, 11, 33) 有三个参数。

注意事项:

虽然在上面的修改后代码中,我们添加了非模板版本的 max 函数,但在 main 中的 max(47, 11, 33) 调用仍然会使用模板版本,因为该调用包含三个参数。为了使用非模板版本处理两个 int 类型的值,你需要分别调用非模板版本,例如:

#include <iostream>

// 两个任意类型的最大值
template<typename T>
T max(T a, T b) {
    std::cout << "max<T>()\n";
    return b < a ? a : b;
}

// 两个 int 值的最大值
int max(int a, int b) {
    std::cout << "max(int,int)\n";
    return b < a ? a : b;
}

// 三个任意类型的最大值
template<typename T>
T max(T a, T b, T c) {
    return max(max(a, b), c);  // 注意这里使用了模板版本
}

int main() {
    // 使用非模板版本处理两个 int 类型的值
    auto result = max(max(47, 11), 33);  // 使用的是 max<int>()

    std::cout << "Result: " << result << std::endl;

    return 0;
}

在这个修改后的示例中,我们通过两次调用非模板版本的 max 函数来处理两个 int 类型的值,然后将结果再次传递给 max 函数来处理第三个 int 类型的值。这样,我们就可以确保使用非模板版本来处理两个 int 类型的值。

详细解释

1. 完美匹配 (1)

  • 完美匹配意味着参数类型与函数参数类型完全相同,或者参数是左值引用且指向相同类型的对象。
    • 在上面的示例中,int yint x 完全匹配,因此这是一个完美匹配。
    • 同样,int yconst int& x 也是完美匹配,因为 y 是一个 int 类型的对象,而 x 是指向 int 类型的左值引用。

2. 匹配需要微调 (2)

  • 数组变量衰减为指向其第一个元素的指针。
    • 在上面的示例中,int array[5] 在传递给 decayArray 函数时会衰减为指向第一个元素的指针。
  • const 添加以匹配 int** 类型的参数和 const int** 类型的参数。
    • int** ptr 在传递给 addConst 函数时需要添加 const 限定符以匹配 const int** 参数。

3. 类型提升匹配 (3)

  • 类型提升是一种隐式转换,包括将小整型转换为更大的整型或浮点型转换为 double
    • 在上面的示例中,short smallIntfloat floatVal 分别被提升为 intdouble 类型,以匹配函数 typePromotion 的参数。

4. 标准转换匹配 (4)

  • 任何类型的标准转换(如 intfloat),或从派生类到其公共、显式基类的转换。
    • 在上面的示例中,int intValue 被转换为 float 类型以匹配函数 standardConversion 的参数。
    • Derived derivedObj 的地址被转换为其基类 Base 的指针,以匹配函数 standardConversion 的第二个参数。

5. 用户定义的转换匹配 (5)

  • 允许任何类型的隐式转换,包括通过构造函数或转换运算符进行的转换。
    • 在上面的示例中,const char* cStr 通过构造函数被转换为 MyString 类型的对象,以匹配函数 userDefinedConversion 的参数。

6. 省略号匹配 (6)

  • 省略号参数几乎可以匹配任何类型,但有一个例外:具有重要复制构造函数的类型可能是有效的,也可能不是有效的(实现可以允许或禁止)。
    • 在上面的示例中,int someInt 通过省略号参数被传递给 variadicArgs 函数。
    • 实际上,省略号参数的匹配非常灵活,但需要注意的是,对于具有重要复制构造函数的类型,某些实现可能不允许将其通过省略号传递。

在这里插入图片描述

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值