Operator Overloading Flashcards

1
Q

How to call an overloaded class operator as a function

A

Ordinarily, we “call” an overloaded operator function indirectly by using the operator on arguments of the appropriate type. However, we can also call an overloaded operator function directly in the same way that we call an ordinary function. We name the function and pass an appropriate number of arguments of the appropriate type:

// equivalent calls to a nonmember operator function
data1 + data2; // normal expression
operator+(data1, data2); // equivalent function call
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

Which operators are not recommended to be overloaded?

A

Ordinarily, the comma, address-of, logical AND, and logical OR operators should not be overloaded.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

What are the general guidelines to define consistent overloaded operators wrt its semantics?

A
  • If the class does IO, define the shift operators to be consistent with how IO is done for the built-in types.
  • If the class has an operation to test for equality, define operator==. If the class has operator==, it should usually have operator!= as well.
  • If the class has a single, natural ordering operation, define operator
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Which operators should be implemented as members and which ones as non members?

A
  • The assignment (=), subscript ([]), call (()), and member access arrow (->) operators must be defined as members.
  • The compound-assignment operators ordinarily ought to be members. However, unlike assignment, they are not required to be members.
  • Operators that change the state of their object or that are closely tied to their given type—such as increment, decrement, and dereference—usually should be members.
  • Symmetric operators—those that might convert either operand, such as the arithmetic, equality, relational, and bitwise operators—usually should be defined as ordinary nonmember functions.

When we define an operator as a member function, then the left-hand operand must be an object of the class of which that operator is a member. For example:

string s = “world”;
string t = s + “!”; // ok: we can add a const char* to a string
string u = “hi” + s; // would be an error if + were a member of string

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

Describe overloading output operator &laquo_space;?

A

Ordinarily, the first parameter of an output operator is a reference to a nonconst ostream object. The second parameter ordinarily should be a reference to const of the class type we want to print.

ostream &operator<

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

Why must IO operators be non member functions?

A

IO operators cannot be members of our own class.

Sales_data data;
data &laquo_space;cout; // if operator&laquo_space;is a member of Sales_data

If these operators are members of any class, they would have to be members of istream or ostream.

IO operators usually must be declared as friends

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Describe overloading input operator&raquo_space; ?

A

The first parameter of an input operator is a reference to the stream from which it is to read, and the second parameter is a reference to the (nonconst) object into which to read.

istream &operator»(istream &is, Sales_data &item)
{
double price; // no need to initialize; we’ll read into price before we use
it
is&raquo_space; item.bookNo&raquo_space; item.units_sold&raquo_space; price;
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
return is;
}

Input operators must deal with the possibility that the input might fail; output operators generally don’t bother. Errors during Input The kinds of errors that might happen in an input operator include the following:
• A read operation might fail because the stream contains data of an incorrect type.
• Any of the reads could hit end-of-file or some other error on the input stream.

if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state

If any of the read operations fails, price will have an undefined value. Therefore, before using price, we check that the input stream is still valid.

Input operators should decide what, if anything, to do about error recovery.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

How do IO operators indicate errors?

A

The input operator might need to set the stream’s condition state to indicate failure, even though technically speaking the actual IO was successful. Usually an input operator should set only the failbit. Setting eofbit would imply that the file was exhausted, and setting badbit would indicate that the stream was corrupted. These errors are best left to the IO library itself to indicate.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

Describe overloading input operator&raquo_space; ?

A

The first parameter of an input operator is a reference to the stream from which it is to read, and the second parameter is a reference to the (nonconst) object into which to read.

istream &operator»(istream &is, Sales_data &item)
{
double price; // no need to initialize; we’ll read into price before we use
it
is&raquo_space; item.bookNo&raquo_space; item.units_sold&raquo_space; price;
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
return is;
}

Input operators must deal with the possibility that the input might fail; output operators generally don’t bother. Errors during Input The kinds of errors that might happen in an input operator include the following:
• A read operation might fail because the stream contains data of an incorrect type.
• Any of the reads could hit end-of-file or some other error on the input stream.

if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state

If any of the read operations fails, price will have an undefined value. Therefore, before using price, we check that the input stream is still valid.

Input operators should decide what, if anything, to do about error recovery.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

Describe how to declare equality operators and its design principles

A

bool operator==(const Sales_data &lhs, const Sales_data &rhs)

  • If a class has an operation to determine whether two objects are equal, it should define that function as operator== rather than as a named function
  • If a class defines operator==, that operator ordinarily should determine whether the given objects contain equivalent data.
  • Ordinarily, the equality operator should be transitive, meaning that if a == b and b == c are both true, then a == c should also be true.
  • If a class defines operator==, it should also define operator!=.
  • One of the equality or inequality operators should delegate the work to the other.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

What are the responsibilities of operator

A
  1. Define an ordering relation that is consistent with the requirements for use as a key to an associative container
  2. Define a relation that is consistent with == if the class has both operators. In particular, if two objects are !=, then one object should be < the other.

If a single logical definition for < exists, classes usually should define the < operator. However, if the class also has ==, define < only if the definitions of < and == yield consistent results.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Describe how to declare equality operators and its design principles

A

bool operator==(const Sales_data &lhs, const Sales_data &rhs)

  • If a class has an operation to determine whether two objects are equal, it should define that function as operator== rather than as a named function
  • If a class defines operator==, that operator ordinarily should determine whether the given objects contain equivalent data.
  • Ordinarily, the equality operator should be transitive, meaning that if a == b and b == c are both true, then a == c should also be true.
  • If a class defines operator==, it should also define operator!=.
  • One of the equality or inequality operators should delegate the work to the other.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

Describe subscript operator

A

The subscript operator must be a member function. To be compatible with the ordinary meaning of subscript, the subscript operator usually returns a reference to the element that is fetched. By returning a reference, subscript can be used on either side of an assignment.

If a class has a subscript operator, it usually should define two versions: one that returns a plain reference and the other that is a const member and returns a reference to const.

class StrVec {
public:
std::string& operator
{ return elements[n]; }
const std::string& operator const
{ return elements[n]; }
// other members as in § 13.5 (p. 526)
private:
std::string *elements; // pointer to the first element in the array
};

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

Describe assignment operator

A

StrVec &StrVec::operator=(initializer_list il);

Unlike the copyand move-assignment operators, this operator does not need to check for selfassignment.

Assignment operators must, and ordinarily compound-assignment operators should, be defined as members. These operators should return a reference to the left-hand operand.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

How to call increment operators explicitly

A

StrBlobPtr p(a1); // p points to the vector inside a1

p. operator++(0); // call postfix operator++
p. operator++(); // call prefix operator++

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

Describe increment and decrement operators

A

Classes that define increment or decrement operators should define both the prefix and postfix versions. These operators usually should be defined as members.

class StrBlobPtr {
public:
	// increment and decrement
	StrBlobPtr&amp; operator++(); // prefix operators
	StrBlobPtr&amp; operator--();
	// other members as before
};

// prefix: return a reference to the incremented/decremented object
StrBlobPtr& StrBlobPtr::operator++()
{
// if curr already points past the end of the container, can’t increment it
check(curr, “increment past end of StrBlobPtr”);
++curr; // advance the current state
return *this;
}

StrBlobPtr& StrBlobPtr::operator–()
{
// if curr is zero, decrementing it will yield an invalid subscript
–curr; // move the current state back one element
check(-1, “decrement past begin of StrBlobPtr”);
return *this;
}

The postfix versions take an extra (unused) parameter of type int. When we use a postfix operator, the compiler supplies 0 as the argument for this parameter. Its sole purpose is to distinguish a postfix function from the prefix version.

// postfix: increment/decrement the object but return the unchanged value

StrBlobPtr StrBlobPtr::operator++(int)
{
// no check needed here; the call to prefix increment will do the check
StrBlobPtr ret = this; // save the current value
++
this; // advance one element; prefix ++ checks the increment
return ret; // return the saved state
}

StrBlobPtr StrBlobPtr::operator–(int)
{
// no check needed here; the call to prefix decrement will do the check
StrBlobPtr ret = this; // save the current value
this; // move backward one element; prefix – checks the
decrement
return ret; // return the saved state
}

17
Q

What are the constraints on the return from operator arrow

A

As with most of the other operators (although it would be a bad idea to do so), we can define operator* to do whatever processing we like.

The same is not true for overloaded arrow. The arrow operator never loses its fundamental meaning of member access. When we overload arrow, we change the object from which arrow fetches the specified member. We cannot change the fact that arrow fetches a member.

When we write point->mem, point must be a pointer to a class object or it must be an object of a class with an overloaded operator->. Depending on the type of point, writing point->mem is equivalent to

(*point).mem; // point is a built-in pointer type
point.operator()->mem; // point is an object of class type

Otherwise the code is in error. That is, point->mem executes as follows:

  1. If point is a pointer, then the built-in arrow operator is applied, which means this expression is a synonym for (*point).mem. The pointer is dereferenced and the indicated member is fetched from the resulting object. If the type pointed to by point does not have a member named mem, then the code is in error.
  2. If point is an object of a class that defines operator->, then the result of point.operator->() is used to fetch mem. If that result is a pointer, then step 1 is executed on that pointer. If the result is an object that itself has an overloaded operator->(), then this step is repeated on that object. This process continues until either a pointer to an object with the indicated member
    is returned or some other value is returned, in which case the code is in error.

The overloaded arrow operator must return either a pointer to a class type or an object of a class type that defines its own operator arrow.

18
Q

Describe member access operators

A
class StrBlobPtr {
	public:
	std::string&amp; operator*() const
	{
		auto p = check(curr, "dereference past end");
		return (*p)[curr]; // (*p) is the vector to which this object points
	}
	std::string* operator->() const
	{
		// delegate the real work to the dereference operator
		return &amp; this->operator*();
	}
	// other members as before
};

Operator arrow must be a member. The dereference operator is not required to be a member but usually should be a member as well.

It is worth noting that we’ve defined these operators as const members. Unlike the increment and decrment operators, fetching an element doesn’t change the state of a StrBlobPtr.

19
Q

What are the constraints on the return from operator arrow

A

As with most of the other operators (although it would be a bad idea to do so), we can define operator* to do whatever processing we like.

The same is not true for overloaded arrow. The arrow operator never loses its fundamental meaning of member access. When we overload arrow, we change the object from which arrow fetches the specified member. We cannot change the fact that arrow fetches a member.

When we write point->mem, point must be a pointer to a class object or it must be an object of a class with an overloaded operator->. Depending on the type of point, writing point->mem is equivalent to

(*point).mem; // point is a built-in pointer type
point.operator()->mem; // point is an object of class type

Otherwise the code is in error. That is, point->mem executes as follows:

  1. If point is a pointer, then the built-in arrow operator is applied, which means this expression is a synonym for (*point).mem. The pointer is dereferenced and the indicated member is fetched from the resulting object. If the type pointed to by point does not have a member named mem, then the code is in error.
  2. If point is an object of a class that defines operator->, then the result of point.operator->() is used to fetch mem. If that result is a pointer, then step 1 is executed on that pointer. If the result is an object that itself has an overloaded operator->(), then this step is repeated on that object. This process continues until either a pointer to an object with the indicated member
    is returned or some other value is returned, in which case the code is in error.

The overloaded arrow operator must return either a pointer to a class type or an object of a class type that defines its own operator arrow.

20
Q

What are the library function objects

A
Arithrnetic
plus
minus
multiplies
divides
moduluse
negate
Relational
equal_to
notequal_to
greater
greater_equal
less
less_equal

Logical
logical_and
logicalor
logical_not

// passes a temporary function object that applies the < operator to two strings
sort(svec.begin(), svec.end(), greater());
sorts the vector in descending order.

21
Q

How is library function objects’s support for pointers?

A

One important aspect of these library function objects is that the library guarantees that they will work for pointers. Recall that comparing two unrelated pointers is undefined. However, we might want to sort a vector of pointers based on their addresses in memory. Although it would be undefined for us to do so directly, we can do so through one of the library function objects:

vector nameTable; // vector of pointers
// error: the pointers in nameTable are unrelated, so < is undefined
sort(nameTable.begin(), nameTable.end(), [](string *a, string *b) { return a < b; });
// ok: library guarantees that less on pointer types is well defined
sort(nameTable.begin(), nameTable.end(), less());

It is also worth noting that the associative containers use less to order their elements. As a result, we can define a set of pointers or use a pointer as the key in a map without specifying less directly.

22
Q

What are std::function operations?

A

function. , f; f is a null function object that can store callable objects with a signature that is equivalent to the function type T ( ie T is relType(args)).
function. , f (nullptr ; Explicitly construct a null function. funct

f (obj ) ; Stores a copy of the callable object obj in

f Use f as a condition; true if f holds a callable object; fa1se otherwise.

f (args) Calls the object in f passing args.

Types defined as members of function

result_type The type returned by this funct ion type’s callable object

argument_type Types defined when T has exactly one or two arguments.

first_argument_type ( second_argument_type ) If T has one argument, argument_type is a synonym that type. If T has two arguments, first argument_type and second_argument_type are synonyms for those argument types.

23
Q

How to store different callable types in a map

A
int add(int i, int j) { return i + j; } // ordinary function
auto mod = [](int i, int j) { return i % j; }; // lambda, which generates an unnamed function-object class
// function-object class
struct div {
	int operator()(int denominator, int divisor)
	{
		return denominator / divisor;
	}
};

Each of these callables applies an arithmetic operation to its parameters. Even though each has a distinct type, they all share the same call signature
int(int, int)

// maps an operator to a pointer to a function taking two ints and returning an int
map binops;
// ok: add is a pointer to function of the appropriate type
binops.insert({"+", add}); // {"+", add} is a pair

binops.insert({“%”, mod}); // error: mod is not a pointer to function

The problem is that mod is a lambda, and each lambda has its own class type. That type does not match the type of the values stored in binops.

map> binops = {
{“+”, add}, // function pointer
{“-“, std::minus()}, // library function object
{“/”, div()}, // user-defined function object
{“*”, { return i * j; }}, // unnamed lambda
{“%”, mod} // named lambda object
};

24
Q

How to assign overloaded functions to a std::function object

A
int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&amp;, const Sales_data&amp;);
map> binops;

int (*fp)(int,int) = add; // pointer to the version of add that takes two ints
binops.insert( {“+”, fp} ); // ok: fp points to the right version of add

// ok: use a lambda to disambiguate which version of add we want to use
binops.insert( {"+", [](int a, int b) {return add(a, b);} });
25
Q

How to store different callable types in a map

A
int add(int i, int j) { return i + j; } // ordinary function
auto mod = [](int i, int j) { return i % j; }; // lambda, which generates an unnamed function-object class
// function-object class
struct div {
	int operator()(int denominator, int divisor)
	{
		return denominator / divisor;
	}
};

Each of these callables applies an arithmetic operation to its parameters. Even though each has a distinct type, they all share the same call signature
int(int, int)

// maps an operator to a pointer to a function taking two ints and returning an int
map binops;
// ok: add is a pointer to function of the appropriate type
binops.insert({"+", add}); // {"+", add} is a pair

binops.insert({“%”, mod}); // error: mod is not a pointer to function

The problem is that mod is a lambda, and each lambda has its own class type. That type does not match the type of the values stored in binops.

map> binops = {
{“+”, add}, // function pointer
{“-“, std::minus()}, // library function object
{“/”, div()}, // user-defined function object
{“*”, { return i * j; }}, // unnamed lambda
{“%”, mod} // named lambda object
};

26
Q

How to properly define class type conversion operator to bool?

A

Because bool is an arithmetic type, a class-type object that is converted to bool can be used in any context where an arithmetic type is expected. Such conversions can happen in surprising ways. In particular, if istream had a conversion to bool, the following code would compile:

int i = 42;
cin &laquo_space;i; // this code would be legal if the conversion to bool were not explicit!

This code could use the bool conversion operator to convert cin to bool. The resulting bool value would then be promoted to int and used as the left-hand operand to the built-in version of the left-shift operator. The promoted bool value (either 1 or 0) would be shifted left 42 positions.

To prevent such problems, the new standard introduced explicit conversion operators:

class SmallInt {
public:
	// the compiler won't automatically apply this conversion
	explicit operator int() const { return val; }
	// other members as before
};

As with an explicit constructor, the compiler won’t (generally) use an explicit conversion operator for implicit conversions:

SmallInt si = 3; // ok: the SmallInt constructor is not explicit
si + 3; // error: implicit is conversion required, but operator int is explicit
static_cast(si) + 3; // ok: explicitly request the conversion

If the conversion operator is explicit, we can still do the conversion. However, with one exception, we must do so explicitly through a cast. The exception is that the compiler will apply an explicit conversion to an expression used as a condition. That is, an explicit conversion will be used implicitly to convert an expression used as

  • The condition of an if, while, or do statement
  • The condition expression in a for statement header
  • An operand to the logical NOT (!), OR (||), or AND (&&) operators
  • The condition expression in a conditional (?:) operator
27
Q

How to avoid ambiguous conversions

A

There are two ways that multiple conversion paths can occur. The first happens when two classes provide mutual conversions. The second way to generate multiple conversion paths is to define multiple conversions from or to types that are themselves related by conversions. The most obvious instance is the built-in arithmetic types.

// usually a bad idea to have mutual conversions between two class types
struct B;
struct A {
	A() = default;
	A(const B&amp;); // converts a B to an A
	// other members
};
struct B {
	operator A() const; // also converts a B to an A
	// other members
};
A f(const A&amp;);
B b;
A a = f(b); // error ambiguous: f(B::operator A())
// or f(A::A(const B&amp;))

Because there are two ways to obtain an A from a B, the compiler doesn’t know which conversion to run; the call to f is ambiguous.

A a1 = f(b.operator A()); // ok: use B’s conversion operator
A a2 = f(A(b)); // ok: use A’s constructor

Note that we can’t resolve the ambiguity by using a cast—the cast itself would have the same ambiguity.

Ambiguities also occur when a class defines multiple conversions to (or from) types that are themselves related by conversions.

struct A {
	A(int = 0); // usually a bad idea to have two
	A(double); // conversions from arithmetic types
	operator int() const; // usually a bad idea to have two
	operator double() const; // conversions to arithmetic types
	// other members
};
void f2(long double);
A a;
f2(a); // error ambiguous: f(A::operator int())
// or f(A::operator double())
long lg;
A a2(lg); // error ambiguous: A::A(int) or A::A(double)

We encounter the same problem when we try to initialize a2 from a long.

  • Standard long to double conversion followed by A(double)
  • Standard long to int conversion followed by A(int)

The call to f2, and the initialization of a2, are ambiguous because the standard conversions that were needed had the same rank. When a userdefined conversion is used, the rank of the standard conversion, if any, is used to select the best match:

short s = 42;
// promoting short to int is better than converting short to double
A a3(s); // uses A::A(int)

In this case, promoting a short to an int is preferred to converting the short to a double. Hence a3 is constructed using the A::A(int) constructor, which is run on the (promoted) value of s.

When two user-defined conversions are used, the rank of the standard conversion, if any, preceding or following the conversion function is used to select the best match.

Correctly designing the overloaded operators, conversion constructors, and conversion functions for a class requires some care.

A few rules of thumb can be helpful:
• Don’t define mutually converting classes—if class Foo has a constructor that takes an object of class Bar, do not give Bar a conversion operator to type Foo.
• Avoid conversions to the built-in arithmetic types. In particular, if you do define a conversion to an arithmetic type, then
– Do not define overloaded versions of the operators that take arithmetic types. If users need to use these operators, the conversion operation will convert objects of your type, and then the built-in operators can be used.
– Do not define a conversion to more than one arithmetic type. Let the standard conversions provide conversions to the other arithmetic types.
• The easiest rule of all: With the exception of an explicit conversion to bool, avoid defining conversion functions and limit nonexplicit constructors to those that are “obviously right.”
struct C {
	C(int);
	// other members
};
struct D {
	D(int);
	// other members
};
void manip(const C&amp;);
void manip(const D&amp;);
manip(10); // error ambiguous: manip(C(10)) or manip(D(10))

Here both C and D have constructors that take an int. Either constructor can be used to match a version of manip. Hence, the call is ambiguous:

manip(C(10)); // ok: calls manip(const C&)

Needing to use a constructor or a cast to convert an argument in a call to an overloaded function frequently is a sign of bad design.

If two (or more) user-defined conversions provide a viable match, the conversions are considered equally good.

struct E {
	E(double);
	// other members
};
void manip2(const C&amp;);
void manip2(const E&amp;);
// error ambiguous: two different user-defined conversions could be used
manip2(10); // manip2(C(10) or manip2(E(double(10)))

In a call to an overloaded function, the rank of an additional standard conversion (if any) matters only if the viable functions require the same userdefined conversion. If different user-defined conversions are needed, then the call is ambiguous.

28
Q

How to avoid ambiguous conversions

A

There are two ways that multiple conversion paths can occur. The first happens when two classes provide mutual conversions. The second way to generate multiple conversion paths is to define multiple conversions from or to types that are themselves related by conversions. The most obvious instance is the built-in arithmetic types.

// usually a bad idea to have mutual conversions between two class types
struct B;
struct A {
	A() = default;
	A(const B&amp;); // converts a B to an A
	// other members
};
struct B {
	operator A() const; // also converts a B to an A
	// other members
};
A f(const A&amp;);
B b;
A a = f(b); // error ambiguous: f(B::operator A())
// or f(A::A(const B&amp;))

Because there are two ways to obtain an A from a B, the compiler doesn’t know which conversion to run; the call to f is ambiguous.

A a1 = f(b.operator A()); // ok: use B’s conversion operator
A a2 = f(A(b)); // ok: use A’s constructor

Note that we can’t resolve the ambiguity by using a cast—the cast itself would have the same ambiguity.

Ambiguities also occur when a class defines multiple conversions to (or from) types that are themselves related by conversions.

struct A {
	A(int = 0); // usually a bad idea to have two
	A(double); // conversions from arithmetic types
	operator int() const; // usually a bad idea to have two
	operator double() const; // conversions to arithmetic types
	// other members
};
void f2(long double);
A a;
f2(a); // error ambiguous: f(A::operator int())
// or f(A::operator double())
long lg;
A a2(lg); // error ambiguous: A::A(int) or A::A(double)

We encounter the same problem when we try to initialize a2 from a long.

  • Standard long to double conversion followed by A(double)
  • Standard long to int conversion followed by A(int)

The call to f2, and the initialization of a2, are ambiguous because the standard conversions that were needed had the same rank. When a userdefined conversion is used, the rank of the standard conversion, if any, is used to select the best match:

short s = 42;
// promoting short to int is better than converting short to double
A a3(s); // uses A::A(int)

In this case, promoting a short to an int is preferred to converting the short to a double. Hence a3 is constructed using the A::A(int) constructor, which is run on the (promoted) value of s.

When two user-defined conversions are used, the rank of the standard conversion, if any, preceding or following the conversion function is used to select the best match.

Correctly designing the overloaded operators, conversion constructors, and conversion functions for a class requires some care.

A few rules of thumb can be helpful:
• Don’t define mutually converting classes—if class Foo has a constructor that takes an object of class Bar, do not give Bar a conversion operator to type Foo.
• Avoid conversions to the built-in arithmetic types. In particular, if you do define a conversion to an arithmetic type, then
– Do not define overloaded versions of the operators that take arithmetic types. If users need to use these operators, the conversion operation will convert objects of your type, and then the built-in operators can be used.
– Do not define a conversion to more than one arithmetic type. Let the standard conversions provide conversions to the other arithmetic types.
• The easiest rule of all: With the exception of an explicit conversion to bool, avoid defining conversion functions and limit nonexplicit constructors to those that are “obviously right.”
struct C {
	C(int);
	// other members
};
struct D {
	D(int);
	// other members
};
void manip(const C&amp;);
void manip(const D&amp;);
manip(10); // error ambiguous: manip(C(10)) or manip(D(10))

Here both C and D have constructors that take an int. Either constructor can be used to match a version of manip. Hence, the call is ambiguous:

manip(C(10)); // ok: calls manip(const C&)

Needing to use a constructor or a cast to convert an argument in a call to an overloaded function frequently is a sign of bad design.

If two (or more) user-defined conversions provide a viable match, the conversions are considered equally good.

struct E {
	E(double);
	// other members
};
void manip2(const C&amp;);
void manip2(const E&amp;);
// error ambiguous: two different user-defined conversions could be used
manip2(10); // manip2(C(10) or manip2(E(double(10)))

In a call to an overloaded function, the rank of an additional standard conversion (if any) matters only if the viable functions require the same userdefined conversion. If different user-defined conversions are needed, then the call is ambiguous.

29
Q

Describe the ambiguities between various function calls and operators overloads, and how to avoid them?

A

a.operatorsym (b); // a has operatorsym as a member function
operatorsym(a, b); // operatorsym is an ordinary function

Unlike ordinary function calls, we cannot use the form of the call to distinquish whether we’re calling a nonmember or a member function.

When we use an overloaded operator in an expression, there is nothing to indicate whether we’re using a member or nonmember function. Therefore, both member and nonmember versions must be considered.

The set of candidate functions for an operator used in an expression can contain both nonmember and member functions.

class SmallInt {
friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0); // conversion from int
operator int() const { return val; } // conversion to int
private:
std::size_t val;
};

SmallInt s3 = s1 + s2; // uses overloaded operator+
int i = s3 + 0; // error: ambiguous

The first addition uses the overloaded version of + that takes two SmallInt values. The second addition is ambiguous, because we can convert 0 to a SmallInt and use the SmallInt version of +, or convert s3 to int and use the built-in addition operator on ints.

Providing both conversion functions to an arithmetic type and overloaded operators for the same class type may lead to ambiguities between the overloaded operators and the built-in operators.