C++ Templates 第二版 附录C 重载决议 英翻中

附录C 重载决议

重载决议就是根据给定的调用表达式来选择函数去调用的过程。考虑如下的简单例子:

void display_num(int); //#1

void display_num(double); //#2

int main()

{

display_num(399); //#1 matches better than #2

display_num(3.99); //#2 matches better than #1

}

在这个例子中,函数名display_num()是被重载的。当这个名字在一个调用中被使用的时候,C++编译器必须根据额外的信息来分辨不同的候选者。大多数情况下,这个信息是调用参数的类型。在我们的例子中,当函数是被整形实参调用时,会去调用int版本的函数,当提供的是浮点型的实参时,double版本的函数会被调用,这是很直观的。这种去为这样的直观选择建模的形式化过程就叫做重载决议过程。

在指导重载决议规则背后的总体思路是十分简单的,但是细节在C++标准化的过程中变得非常复杂。造成这种复杂度的原因,主要是想要支持各种在现实中就很直观的“明显的最佳匹配”的例子(对人类来说),然而在尝试去形式化这种直觉的时候,出现了很多微妙之处。

在本附录中,我们提供一个细节合理的关于重载决议规则的综述。然而,出于这一过程的复杂度考虑,我们不会涵盖这一话题的每一部分。

 

C1. 重载决议何时生效?

重载决议是完整的函数调用过程的一部分。事实上,它不是每一次函数调用都会有的过程。首先,通过函数指针和指向成员函数的指针来调用时不会触发重载决议,因为要调用的函数通过指针已经完全确定了(在运行期)。其次,像函数一样的宏不会被重载,因此不会触发重载决议。

高度总结一下,调用一个命名函数会被按如下方式处理:

1. 寻找该名字,组成一个初始的"重载集合"

2. 必要的话,该集合会通过多种方式来调整(例如,模板实参的推断和替换时,会导致一些函数模板候选者被丢弃)

3. 所有不完全匹配调用的候选者(即使在考虑了隐式转换和默认实参之后)会从重载集合中排除掉。结果会形成可用的候选函数集合。

4. 执行重载决议来找到一个最佳的候选函数。如果存在一个,就选择他。否则,这个调用就是引起歧义的。

5. 检查被选择的候选函数。例如,如果它是一个被删除的函数(i.e. 以=delete定义)或是一个不可用的私有成员函数,则会报告诊断出问题。

每一个步骤都有其微妙之处,不过重载决议可能是最复杂的。幸运的是,一些简单的原则可以阐明大多数的情况。接下来我们将检查这些原则。

 

C2. 简化的重载决议

重载决议会将可用的候选函数按照每个调用的实参与该候选函数相对应的形参的匹配程序来排列。如果一个候选函数要优于另一个,那么它的任意一个参数的匹配都不能劣于另一个候选函数相对应的参数匹配。下面举例说明:

void combine(int, double);

void combine(long, int);

int main()

{

combine(1, 2); //ambiguous

}

在这个例子中,调用combine()是有歧义的,因为第一个候选函数匹配第一个参数(int类型的数字1)是最优的,然而第二个候选函数匹配第二个参数是最优的。我们会争辩说在某种程度上int距离long比举例double更近(也就是支持选择第二个候选函数),但是C++不会在涉及到多个调用实参匹配的情况下,去定义匹配的接近程度(译者注: 也就是说所有算术类型转换的级别都一样)。

根据第一个原则,我们还需要指定一个给定的实参与候选函数对应的形参的匹配程度。大致上,我们可以按如下所示来排列匹配程度(从最好的到最坏的)

1.完美匹配。参数就是表达式的类型,或者是表达啊是类型的引用(也可以加上const 或 volatile的限定符).

2.通过微小的调整来匹配。距离,包括一个数组变量退化为一个指向其首元素的指针或者添加const使实参类型int** 来匹配形参类型int const* const*

3.通过类型提升来匹配。类型提升是一种隐式转换,包括从小的整数类型(例如bool,char,short,有时包括枚举类型)转换为int,unsigned int, long, unsigned long类型,以及float转换为double

4.仅通过标准转换来匹配。这包括任意种类的标准转换(例如int转为float)或者从继承类转换为一个公用的(public),明确的基类,但是要排除对类型转换运算符和转换构造函数的隐式调用。

5.通过用户定义的转换来匹配。这允许任意种类的隐式转换。

6.通过省略号来匹配(...)。省略号形参几乎可以匹配任意类型。不过,存在一个例外,拥有非平凡的拷贝构造函数(nontrivial copy constructor)的类类型不一定是有效的(具体实现可以自由的允许或禁止这种匹配).

下面设计的例子举例说明了其中的一些匹配:

int f1(int); //#1

int f1(double); //#2

f1(4); //calls #1: perfect match(#2 requires a standard conversion)

 

int f2(int); //#3

int f2(char); //#4

f2(true); //calls #3: match with promotion (#4 requires stronger standard conversion)

 

class X{

public:

X(int);

}

int f3(X); //#5

int f3(...); //#6

f3(7); //class #5:match with user-defined conversion (#6 requires a match with ellipsis)

注意,重载决议发生在模板实参推断之后,并且推断时不会考虑上述所有的类型转换。举例:

template <typename T>

void MyString

{

public:

MyString(const T*); //converting constructor

}

 

template <typename T>

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

 

int main()

{

MyString<char> str1, str2;

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

str2 = truncate("Hello World", 5); //ERROR

}

在进行模板实参推断的时候,不会将类型转换构造函数提供的隐式转换考虑在内。给str2的赋值找不到可用的truncate()函数,因此重载决议根本不会被执行。

在模板实参推断的时候,要记得一个右值引用的模板形参既可以推断为左值引用类型(在引用折叠之后)(如果对应的实参时一个左值)也可以推断为一个右值引用类型(如果实参是一个右值)(见277页15.6章节)。举例:

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&&

}

虽然前面讲的只是大致上的准则,但是他们可以涵盖很多情况。不过,仍有许多情况无法用这些规则来进行充分的解释。接下来我们开始针对这些规则最重要的改进来进行简短的讨论。

 

 

C2.1 成员函数的隐式实参

非静态的成员函数的调用会有一个隐藏的参数,可以在成员函数定义时通过*this来使用。对于类MyClass的一个成员函数来讲,隐藏的参数通常是MyClass&类型(针对非常量成员函数)或const MyClass&类型(针对常量成员函数)(注1)。有点让人惊讶的是this是一个指针类型,若是让之前的this等价于现在的*this会更好些。然而,this是在引用类型加入到C++语言之前就存在于C++早期版本中的,当引用类型加入时,已有太多依赖this是指针类型的代码了。

注1: 如果成员函数用volatile来修饰,则隐藏参数也可能是类型volatile MyClass& 或者 const volatile MyClass&类型,只不过这很不常见。

这个隐藏的*this参数会参与重载决议,就如同显式的参数一样。在大多数情况下这都是十分自然的,不过偶尔也会让人感到意外。下面的例子展示了一个类似String的类,它不会按照设计的意图来工作(在现实中我们也看到过太多这种代码了):

#include <cstddef>

class BadString

{

public:

BadString(const char*);

//character access through subscripting:

char& operator[](std::size_t); #1

const char& operator[](std::size_t) const;

 

//implicit conversion to null-terminated byte string:

operator char*(); #2

operator const char*();

};

 

int main()

{

BadString str("correkt");

str[5] = 'c'; //possibly an overload resolution ambiguity!

}

一开始,表达式str[5]似乎是不具有二义性的。#1中的下标运算符似乎是一个完美匹配。然而,这不是特别的完美,因为实参5是int类型,而操作符期望一个无符号的整数类型(size_t和std::size_t通常都是unsigned int 或 unsigned long类型,但绝不会是int类型).尽管如此,一个简单的标准证书转换就使得#1的可行的。然而,也存在另一个可行的候选函数:内建的下标运算符。 的确,如果我们对str来应用隐式转换运算符(str是隐式的成员函数实参),我们会获得一个指针类型,而内建的下标运算符可应用于指针类型。内建的运算符的参数类型是ptrdiff_t,这在许多平台上等价于int,因此对于参数5就是一个完美匹配。所以,即使内建下标运算符对于隐式实参来说是一个不好的匹配(通过用户定义的类型转换),不过对于实际的下标来说,内建的下标运算符相比#1定义的运算符是一个更优的匹配。所以这就是潜在的二义性(注2)。要解决这种问题且具有可移植性,你可以用ptrdiff_t的参数类型来声明[]运算符,或者你可以用显式(explicit)的转换到char*来代替隐式的类型转换(总之,这是通常的推荐做法).

注2: 注意,二义性只存在于size_t同义于unsigned int的平台上。对于size_t同义于unsigned long且类型ptrdiff_t是long的类型别名的平台,不会存在二义性,因为对于下标表达式,内建的下标运算符同样需要类型转换。

译者注: 感觉应该是ptrdiff_t为int的平台才会存在二义性,因为此时内建的下标运算符的下标5是完美匹配了呀

可行的候选函数中可能既包含了静态成员也包含了非静态成员。当静态成员和非静态成员进行比较时,隐式实参的匹配成都是被忽略的(只有非静态的成员拥有隐式*this实参).

默认情况下,一个非静态的成员函数有一个隐式的*this参数,它的类型是左值引用,不过C++11引入了使其成为右值引用的语法。举例:

struct S

{

void f1(); //implicit *this parameter is an lvalue reference(see below)

void f2() &&; //implicit *this parameter is an rvalue reference

void f3() &; //implicit *this parameter is an lvalue reference

};

从这个例子可以看出,不仅可以使隐式的参数的类型为右值引用(通过&&后缀),也可以使其确认为左值引用(通过&后缀).有趣的是,指定&后缀并不完全等价于省略它的时候:一个老的特例允许当一个引用是传统的隐式的*this参数的时候,可以绑定一个右值到该非const的左值引用上. 但是如果显式的要求将*this作为左值引用来对待,这种特例(有点危险的)就不再适用了。所以,根据上述S的定义:

int main()

{

S().f1(); // OK: old rule allows rvalue S() to match implied

// lvalue reference type S& of *this

S().f2(); // OK: rvalue S() matches rvalue reference type

// of *this

S().f3(); // ERROR: rvalue S() cannot match explicit lvalue

// reference type of *this

}

 

C2.2 改善完美匹配

对于类型为X的实参来说,有四种普通的形参类型可以构成完美匹配: X, X&, const X& 和X&&(const X&&也是一个精确匹配,不过这很少用到).不过,使用其中两种引用来重载函数是很常见的。在C++11之前,这意味着会有这样的情况:

void report(int&); //#1

void report(const int&); //#2

 

int main()

{

for(int k = 0; k < 10; ++k)

{

report(k); //calls #1

}

report(42); //calls #2

}

这里,左值会选择没有额外const的版本,然而带const的版本则只能匹配右值。

随着在C++11中加入了右值引用,出现了另一种需要区分两种完美匹配的场景,由下面的例子阐明:

struct Value{};

void pass(const Value&); //#1

void pass(Value&&); //#2

 

void g(X&& x)

{

pass(x); //calls #1, because x is an lvalue

pass(X()); //calls #2, because X() is an rvalue(in fact, prvalue)

pass(std::move(x)) //calls #2, because std::move(x) is an rvalue(in fact, xvalue)

}

此时,带有右值引用的版本对于右值来说是一个更好的匹配,不过它不嫩匹配左值。

注意,这同样适用于成员函数调用中的隐式实参:

class Wonder

{

public:

void tick(); //#1

void tick() const; //#2

void tack() const; //#3

};

 

void run(Wonder& device)

{

device.tick(); //calls #1

device.tack(); //calls #3, because there is no non-const version of Wonder::tack()

}

最后,下面对于更早前的例子的修改说明了如果使用带引用和不带引用的重载方式,两个完美的匹配同样会产生二义性:

void report(int); // #1

void report(int&); // #2

void report(int const&); // #3

 

int main()

{

for (int k = 0; k<10; ++k)

{

report(k); // ambiguous: #1 and #2 match equally well

}

report(42); // ambiguous: #1 and #3 match equally well

}

 

C.3 重载细节

前面章节涵盖了日常C++编程中会遇到的绝大多数的重载情况。不幸的是,还有更多的规则以及这些规则的例外,将这些在一本不是专门讲C++函数重载的书中呈现是不合理的。尽管如此,在此我们会讨论其中的一些,因为它们比其他规则应用的更加频繁,也可以让我们对重载的细节有更多的认识。

 

C3.1 选择非模板或更加特化的模板

在重载决策中,当所有其他方面都相等的时候,会选择一个非模板的函数而不是一个模板实例(不管这个实例是通过泛型的模板定义生成的还是显式特化的。举例:

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

}

这个例子同样清楚的阐明了重载决议通常不会涉及被选择函数的返回类型。

然而,在重载决议中其他方面有轻微的区别时(例如不同的const或引用修饰),会首先应用一般的规则。如果成员函数被定义接受相同的实参,如同拷贝和移动构造函数那样,这个效果容易偶尔造成让人惊讶的行为。细节见333页的16.2.4章节。

如果要在两个模板之间做出选择,那么最特化的模板会被选择(假如一个确实是比另一个更特化).对这个概念详尽的解释见330页的16.2.2章节。这个特性的一个特例是当两个模板的不同之处只是其中一个存在可变长参数时,那么不带可变长参数的模板就被认为是更加特化的,因此如果它匹配调用,就会被选择。57的4.1.2章节会讨论这种情形的一个例子。

 

C3.2 类型转换的序列

通常,一个隐式的转换可能是一系列基本的转换。考虑下面的例子:

class Base

{

public:

operator short() const;

};

class Derived : public Base{};

void count(int);

void process(const Derived& object)

{

count(object); //matches this user-defined conversion

}

调用count(object)之所以能正常工作,是因为object可以隐式地转换为int. 然而,这个转换会经历如下若干步骤:

1. 将object从const Derived转换为const Base(这是泛左值转换; 它保留了object的标识)

2. 根据用户定义的转换将const Base类型的object转换为类型short

3. 将short提升为int

这是最通用的一种转换序列:一个标准转换(在这里是派生类到基类转换),后面是一个用户定义的转换,再后面是另一个标准转换。尽管在一个转换序列中最多只能有一个用户定义的转换,不过只包含标准转换也是可能的。

一个重要的原则是,如果一个转换序列是另一个转换序列的子序列,那么相比于后者,重载决议会选择前者。如果在这个例子中有一个附加的候选函数

void count(short);

则会选择这个附加的函数来调用count(object),因为它不需要转换序列中的第三步(类型提升).

 

C3.3 指针的类型转换

指针以及成员指针会进行集中特殊的标准转换,包括

●转换为bool类型

●从任意的指针类型转换为void*

●指针类型从子类转换为基类

●成员指针的类型从基类转换为子类

虽然,所有这些转换都能"仅通过标准转换进行匹配",但他们并不是等价的

首先,转换为bool类型(不管是从常规的指针还是从成员指针)被认为是糟糕于任何其他种类的标准转换的. 例如:

void check(void*); #1

void check(bool); #2

 

void rearrange(Matrix* m)

{

check(m); //calls #1

}

在常规指针的类型转换中,转换为类型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

}

很直观的是,从Machine*转换为CommonProcesses*要优于从Machine*转换为Interface*.

一个非常相似的规则适用于成员指针: 在两个相关联的成员指针的类型转换中,在继承图中"最靠近基类“的(i.e.派生层次最低的)会被优先选择

译者注:这是因为成员指针只能从基类向下转为子类,举个自己写的例子:

#include <iostream>

class SS{};

class Interface

{

public:

SS m_ss;

};

class Common : public Interface{};

class Machine : public Common{};

 

using InterfaceType   = const SS Interface::*;

using CommonType  = const SS Common::*;

using MachineType = const SS Machine::*;

 

void serialize(CommonType) { std::cout << "Call CommonType" << std::endl; }

void serialize(MachineType) { std::cout << "Call MachineType" << std::endl; }

 

int main(int argc, char* argv[])

{

InterfaceType interface = &Interface::m_ss;

serialize(interface);

}

以上程序输出"Call CommonType"

 

C3.4 初始化列表

初始化列表作为实参(通过花括号传递的初始化器)时可以被转换为若干个不同种类的参数:initializer_lists,带有初始化列表构造函数的类,初始化列表中的元素可以被当作构造函数中(不同的)参数的类,或者成员可以被初始化列别中的元素初始化的聚合类。下面的程序阐明了这些情况:

#include <initializer_list>

#include <string>

#include <vector>

#include <complex>

#include <iostream>

 

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>&)

{

std::cout << "#3\n";

}

会导致

void h(const std::complex<double>&)

{

std::cout << "#4\n";

}

 

struct Point

{

int x, y;

};

 

void i(const Point& pt)

{

std::cout << "#5\n";

}

 

int main()

{

f({ 1, 2, 3 });

f({ "hello", "initializer", "list" });

g({ 1, 1, 2, 3, 5 });

h({ 1.5, 2.5 });

i({ 1, 2 });

}

在最开始的两个f()调用中,实参初始化列表被转换为类型std::initializer_list的值,这包含将初始化列表中的每个元素转换为std::initializer_list的元素类型。在第一个调用中,所有的元素已经是int类型了,所以不需要额外的转换。在第二个调用中,初始化列表中的每个字符串字面量要通过std::string的构造函数string(const char*)转换为std::string.第三个调用(g())会通过std::vector(std::initializer_list<int>)执行一个用户定义的转换。下一个调用会调用std::complex(double, double)的构造函数,就如同这么写一样std::complex<double>(1.5, 2.5).最后一个调用会执行聚合初始化,这会用初始化列表中的元素来初始化类Point的一个实例中的成员,而不会去调用Point的构造函数(注3).

注3:在C++中,聚合初始化仅仅可用于聚合类。所谓聚合类是指数组,或者没有用户提供的构造函数的、没有private或protectd的非静态成员的、没有基类并且没有虚函数的简单的类似C语言的类。在C++14之前,聚合类不能有默认的成员初始化器。C++17以来,公共的基类也允许了。

关于初始化列表,有几个有趣的重载情形。当转换初始化列表为一个initializer_list时,就像前面的例子中最开始的两个调用,整个转换的匹配优劣程度和初始化列表中的每个元素转换为initializer_list的元素类型(i.e. initializer_list<T>中的T)时,其中最糟糕的转换相同。这有些令人惊讶,就像下面这个例子:

#include <initializer_list>

#include <iostream>

 

void ovl(std::initializer_list<char>) //#1

{

std::cout << "#1\n";

}

 

void ovl(std::initializer_list<int>) //#2

{

std::cout << "#2\n";

}

 

int main()

{

ovl({ 'h', 'e', 'l', 'l', 'o', '\0' }); //prints #1

ovl({ 'h', 'e', 'l', 'l', 'o', 0 }); //prints #2

}

在第一次调用ovl()时,初始化列表中的每个元素都是char。对于第一个ovl()函数,这些元素不需要任何转换。对于第二个ovl()函数,这些元素要被提升为int.由于完美匹配是要优于类型提升的,所以第一个调用ovl()调用#1

在第二次调用ovl()时,开始的五个元素是类型char,最后一个是类型int。对于第一个ovl()函数,char类型元素是完美匹配,不过int要求一个标准转换,所以整个转换就和标准转换相同。对于第二个ovl()函数,char类型元素要被提升为int,最后一个int类型元素是完美匹配。对于第二个ovl()函数整个转换就和提升相同,这使其成为优于第一个ovl()的候选函数,即使只有一个元素的转换是更好的.

当通过初始化列表初始化类的对象时,就像前面的例子中调用g()和h()那样,重载决议会分两个阶段进行:

1. 第一个阶段只考虑初始化列表构造函数,就是对于类型T(移除了顶层的引用和const/volatile修饰)来说,构造函数唯一的非默认的参数就是类型std::initializer_list<T>

2. 如果没有这样可用的构造函数,那么第二个阶段会考虑所有其他的构造函数。对于这个规则有个例外:如果初始化列表是空的并且这个类有一个默认构造函数,那么会跳过第一步,调用默认构造函数。

由于这条规则,任何初始化列表构造函数都比其他非初始化列表构造函数要优先考虑,如同下面的例子所说明的:

#include <initializer_list>

#include <iostream>

#include <string>

 

template<typename T>

struct Array

{

Array(std::initializer_list<T>)

{

std::cout << "#1\n";

}

 

Array(unsigned n, const T&)

{

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 #1

arr2({ "Hello", "initializer", "list" }); //prints #1

arr2({ 10, "hello" }); //prints #2

}

注意,第二个需要unsigned和const T&的构造函数,在通过初始化列表初始化Array<int>时,是不会被调用的,因为它的初始化列表构造函数总是优于它的非初始化列表构造函数。然而,至于Array<string>来说,当初始化列表构造函数不可用时,非初始化列表构造函数会被调用,就如同第二次调用arr2()的情况。

 

C.3.5 仿函数和替代函数

我们前面提到过在查找函数名之后会创建一个初始的重载集合,这个集合会通过多种方式调整。当调用表达式引用的是类类型对象而不是函数时,会出现有趣的情况。在这种情况下,重载集合会有两种潜在的新增情况:

第一种附加是明确的:任何成员运算符()(函数调用运算符)都会被加入到集合中。包含这种运算符的对象通常被称为仿函数(functors)或函数对象(见157页的11.1章节)。

另一种不是那么明显的附加情况是当一个类类型对象包含一个隐式的可转换为指向函数类型的指针(或者是函数类型的引用)的转换运算符时(注4)。在这种情形中,一个仿函数(或替代函数)会被加入到重载集合中。这个候选的替代函数的参数被认为除了对应转换函数的目的函数类型所带的参数之外,还有一个隐式的由转换函数指定类型的形参。一个例子可使这个概念更清晰:

注4: 在这种情况下转换运算符必须也是可用的。例如一个非常量(non-const)的运算符对于常量对象的不可用的。

using FuncType = void(double, int);

 

class IndirectFunctor

{

public:

void operator()(double, double) const;

operator FuncType* () const;

};

 

void activate(const IndirectFunctor& funcObj)

{

funcObj(3, 5); //ERROR: ambiguous

}

调用funcObj(3, 5)是被当做有3个实参的调用: funcObj, 3和5. 可用的候选函数包括成员运算符()(这被当做拥有参数类型const IndirectFunctor&, double和double)和一个参数类型为FuncType*, double和int的替代函数.替代函数关于隐式参数是一个糟糕的匹配(因为它要求一个用户定义的转换),不过对于对于最后一个参数则是一个较好的匹配。因此,这两个候选函数无法被排序,所以这个调用是具有二义性的。

替代函数处于C++中最昏暗的角落里,在实践中几乎不会用到(幸运的是).

 

C.3.6 其它的重载情况

到目前为止,我们讨论的重载是关于在一个调用表达式中如何决定调用哪个函数。然而,在一些其它的情况中,也需要做一个类似的选择。

第一种情况出现在当需要一个函数的地址的时候。考虑下面的例子:

int numElems(const Matrix&); //#1

int numElems(const Vector&); //#2

 

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

这里,名字numElems涉及到一个重载集合,不过集合中只有一个函数的地址是可取的。重载决议会尝试去用所要求的函数类型(本例中是funcPtr的类型)去匹配可用的候选函数。

另一种需要重载决议的情况是在初始化时。不幸的是,这是一个充满了微妙之处的话题,超出了一个附录能覆盖的范围。然而,下面这个简单的例子可以稍微说明一下重载决议的这个附加方面。

#include <string>

class BigNum

{

public:

BigNum(long n); //#1

BigNum(double n); //#2

BigNum(const std::string&); //#3

operator double(); //#4

operator long(); //#5

};

 

void initDemo()

{

BigNum bn1(100103L); //selects #1

BigNum bn2("1232434"); //selects #3

int in = bn1; //selects #5

}

在这个例子中,需要重载决议去选择适当的构造函数或转换运算符。明确地,bn1的初始化会调用第一个构造函数,bn2会调用第三个构造函数,in会调用operator long()。在绝大多数情况下,重载规则会造成直观的结果。然而,这些规则的细节是十分复杂的,一些应用程序所依赖的是C++语言中更加偏僻的知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值