Movatterモバイル変換


[0]ホーム

URL:




Chapter 11: More Operator Overloading

Having covered the overloaded assignment operator in chapter9, andhaving shown several examples of other overloaded operators as well (i.e., theinsertion and extraction operators in chapters3 and6), we now take a look atoperator overloading in general.

11.1: Overloading `operator[]()'

As our next example of operator overloading, we introduce a classIntArrayencapsulating an array ofints. Indexing the array elements is possibleusing the standard array index operator[], but additionally checks forarray bounds overflow are performed (note, however, that index checking isnot normally done by index operators. Since it's good practice to avoidsurprises array bound checks should normally not be performed by overloadedindex operators). Theindex operator (operator[]) is interesting because it can be used inexpressions as bothlvalue and asrvalue.

Here is an example illustrating the basic use of the class:

    int main()    {        IntArray x{ 20 };               // 20 ints        for (int idx = 0; idx < 20; ++idx)            x[idx] = 2 * idx;                   // assign the elements        for (int idx = 0; idx <= 20; ++idx)     // result: boundary overflow            cout << "At index " << idx << ": value is " << x[idx] << '\n';    }
First, the constructor is used to create an object containing 20ints. The elements stored in the object can be assigned or retrieved. Thefirstfor-loop assigns values to the elements using the index operator,the secondfor-loop retrieves the values but also results in a run-timeerror once the non-existing valuex[20] is addressed. TheIntArrayclass interface is:
    #include <cstddef>    class IntArray    {        size_t d_size;        int     *d_data;         public:            IntArray(size_t size = 1);            IntArray(IntArray const &other);            ~IntArray();            IntArray &operator=(IntArray const &other);                                                // overloaded index operators:            int &operator[](size_t index);              // first            int const &operator[](size_t index) const;  // second            void swap(IntArray &other);         // trivial        private:            void boundary(size_t index) const;            int &operatorIndex(size_t index) const;    };
This class has the following characteristics:

Now, the implementation of the members (omitting the trivialimplementation ofswap, cf. chapter9) are:

    #include "intarray.ih"    IntArray::IntArray(size_t size)    :        d_size(size)    {        if (d_size < 1)            throw "IntArray: size of array must be >= 1"s;        d_data = new int[d_size];    }    IntArray::IntArray(IntArray const &other)    :        d_size(other.d_size),        d_data(new int[d_size])    {        memcpy(d_data, other.d_data, d_size * sizeof(int));    }    IntArray::~IntArray()    {        delete[] d_data;    }    IntArray &IntArray::operator=(IntArray const &other)    {        IntArray tmp(other);        swap(tmp);        return *this;    }    int &IntArray::operatorIndex(size_t index) const    {        boundary(index);        return d_data[index];    }    int &IntArray::operator[](size_t index)    {        return operatorIndex(index);    }    int const &IntArray::operator[](size_t index) const    {        return operatorIndex(index);    }    void IntArray::swap(IntArray &other)    {        // swaps the d_size and d_data data members        // of *this and other    }    void IntArray::boundary(size_t index) const    {        if (index < d_size)            return;        ostringstream out;        out  << "IntArray: boundary overflow, index = " <<                index << ", should be < " << d_size << '\n';        throw out.str();    }
Note how theoperator[] members were implemented: as non-const membersmay call const member functions and as the implementation of theconstmember function is identical to the non-const member function's implementationbothoperator[] members could be defined inline using an auxiliaryfunctionint &operatorIndex(size_t index) const. Aconst memberfunction may return a non-const reference (or pointer) return value, referringto one of the data members of its object. Of course, this is a potentiallydangerous backdoor that may break data hiding. However, the members in thepublic interface prevent this breach and so the two publicoperator[]members may themselves safely call the sameint &operatorIndex() constmember, that defines aprivate backdoor.

11.1.1: Multi-argument `operator[]()'

Consider a standard two-dimensional array. It hasnRows rows andnColscolumns. Such an array is a generalization of a one-dimensional array: eachrow consists of an array ofnCols elements. If a typeDoubleArray isavailable, defined like theIntArray of the previous section, butcontainingdouble values instead ofint values, then designing a classMatrix could start as follows:
    class Matrix    {        size_t d_nRows;        size_t d_nCols;        DoubleArray *d_row;        public:            Matrix(size_t nRows, size_t nCols);        ...    };
and its constructor allocatesnRows DoubleArrays, each havingnCols columns, with eachDoubleArray being initialized to anColselements, initialized to 0 by theDoubleArray's default constructor:
    Matrix::Matrix(size_t nRows, size_t nCols)    :        d_nRows(nRows),        d_nCols(nCols),        d_row(new DoubleArray[nRows])    {}

Traditionally accessing elements of aMatrix could be realized in threeways (plus optionally correspondingconst variants):

The memberselement androw work fine, but at the disadvantage thatthe standard syntax for referring to matrix elements is not used. E.g, toaccess elementmatrix[3, 4] we would writematrix.element(3, 4) ormatrix.row(3)[4], whereas the third element requires us to use two indexoperators:matrix[3][4]. Moreover, with the second and third members datahiding ofDoubleArray row is abandoned if the only reason for thesemembers is to accessMatrix elements.

operator[]The overloaded index operator, however, can also be defined having multiplearguments, allowing the use of the (standard mathematical) syntaxmatrix[row, col] (and in general: the overloaded index operator can alsohave more arguments, which might come in useful when defining, e.g., arrays ofarrays).

ProvidingMatrix with an index operator accepting two arguments is simple:just add a memberdouble &operator[](size_t row, size_t col) (andoptionally a comparableconst member) to the class's interface. Itsimplementation may then directly return the requested array element:

    double &Matrix::operator[](size_t row, size_t col)    {        return d_row[row][col];    }

As an aside: note that this implementation does not check whether the providedindices are valid. Traditionally index operators don't perform such checks,improving the efficiency of programs when it'sknown that indicescannot be invalid. E.g., to initialize all elements of aMatrix withsubsequent integral values the following function could be used:

    void init(Matrix &matrix, size_t value)    {        for (size_t row = 0; row != matrix.nRows(); ++row)            for (size_t col = 0; col != matrix.nCols(); ++col)                matrix[row, col] = value++;    }

If, on the other hand, checking the validity of the indicesis necessarythen a multi-argumentat member like the following can be defined:

    double &Matrix::at(size_t row, size_t col)    {        if (row >= d_nrows or col >= d_nCols)            throw runtime_error("Matrix::at: invalid indices");        return d_row[row][col];    }

11.2: Overloading insertion and extraction operators

Classes may be adapted in such a way that their objects may be insertedinto and extracted from, respectively, astd::ostream andstd::istream.

The classstd::ostream defines insertion operators for primitivetypes, such asint,char *, etc.. In this section we learn how toextend the existing functionality of classes (in particularstd::istreamandstd::ostream) in such a way that they can be used also in combinationwith classes developed much later in history.

In particular we will show how theinsertion operator can be overloaded allowing the insertion of any type ofobject, sayPerson (see chapter9), into anostream. Havingdefined such an overloaded operator we're able to use the following code:

    Person kr("Kernighan and Ritchie", "unknown", "unknown");    cout << "Name, address and phone number of Person kr:\n" << kr << '\n';

The statementcout <<kr usesoperator<<. This memberfunction has two operands: anostream & and aPerson &. The requiredaction is defined in an overloadedfree functionoperator<< expecting twoarguments:

                                // declared in `person.h'    std::ostream &operator<<(std::ostream &out, Person const &person);                                // defined in some source file    ostream &operator<<(ostream &out, Person const &person)    {        return            out <<                "Name:    " << person.name() << ", "                "Address: " << person.address() << ", "                "Phone:   " << person.phone();    }

The free functionoperator<< has the following noteworthy characteristics:

In order to overload theextraction operator for, e.g., thePersonclass, members are needed modifying the class's private data members. Suchmodifiers are normally offered by the class interface. ForthePerson class these members could be the following:

    void setName(char const *name);    void setAddress(char const *address);    void setPhone(char const *phone);

These members may easily be implemented: the memorypointed to by the corresponding data member must be deleted, and the datamember should point to a copy of the text pointed to by the parameter. E.g.,

    void Person::setAddress(char const *address)    {        delete[] d_address;        d_address = strdupnew(address);    }

A more elaborate function should check the reasonableness of the newaddress (address also shouldn't be a 0-pointer). Thishowever, is not further pursued here. Instead, let's have a look at the finaloperator>>. A simple implementation is:

    istream &operator>>(istream &in, Person &person)    {        string name;        string address;        string phone;        if (in >> name >> address >> phone)    // extract three strings        {            person.setName(name.c_str());            person.setAddress(address.c_str());            person.setPhone(phone.c_str());        }        return in;    }

Note the stepwise approach that is followed here. First, the requiredinformation is extracted using available extraction operators. Then, if thatsucceeds,modifiers are used to modify the data members of the object tobe extracted. Finally, the stream object itself is returned as a reference.

11.3: Conversion operators

A class may be constructed around a built-in type. E.g., a classString, constructed around thechar * type. Such a class may defineall kinds of operations, like assignments. Take a look at the following classinterface, designed after thestring class:
    class String    {        char *d_string;        public:            String();            String(char const *arg);            ~String();            String(String const &other);            String &operator=(String const &rvalue);            String &operator=(char const *rvalue);    };

Objects of this class can be initialized from achar const *, and alsofrom aString itself. There is an overloaded assignment operator, allowingthe assignment from aString object and from achar const* (Note that the assignment from achar const * also allows thenull-pointer. An assignment likestringObject = 0 is perfectly in order.).

Usually, in classes that are less directly linked to their data than thisString class, there will be anaccessor member function,like a memberchar const *String::c_str() const. However, the need to usethis latter member doesn't appeal to our intuition when an array ofStringobjects is defined by, e.g., a classStringArray. If this latter classprovides theoperator[] to access individualString members, it wouldmost likely offer at least the following class interface:

    class StringArray    {        String *d_store;        size_t d_n;        public:            StringArray(size_t size);            StringArray(StringArray const &other);            StringArray &operator=(StringArray const &rvalue);            ~StringArray();            String &operator[](size_t index);    };

This interface allows us to assignString elements to each other:

    StringArray sa{ 10 };    sa[4] = sa[3];  // String to String assignment

But it is also possible to assign achar const * to an element ofsa:

    sa[3] = "hello world";

Here, the following steps are taken:

Now we try to do it the other way around: how toaccess thechar const * that's stored insa[3]? The following attempt fails:
    char const *cp = sa[3];

It fails since we would need an overloaded assignment operator for the'class'char const *. Unfortunately, there isn't such a class, andtherefore we can't build that overloaded assignment operator (see also section11.15). Furthermore,casting won't work as thecompiler doesn't know how to cast aString to achar const *. Howto proceed?

One possibility is to define an accessor member functionc_str():

    char const *cp = sa[3].c_str()

This compiles fine but looks clumsy.... A far better approach would be touse aconversion operator.

Aconversion operator is a kind of overloaded operator, but this timethe overloading is used to cast the object to another type.In class interfaces, the general form of aconversion operator is:

    operator <type>() const;

Conversion operators usually areconst member functions: they areautomatically called when their objects are used asrvalues in expressionshaving atypelvalue. Using a conversion operator aStringobject may be interpreted as achar const * rvalue, allowing us to performthe above assignment.

Conversion operators are somewhat dangerous. The conversion is automaticallyperformed by the compiler and unless its use is perfectly transparent it mayconfuse those who read code in which conversion operators are used. E.g.,noviceC++ programmers are frequently confused by statements like `if(cin) ...'.

As arule of thumb: classes should define at most one conversionoperator. Multiple conversion operators may be defined but frequently resultin ambiguous code. E.g., if a class definesoperator bool() const andoperator int() const then passing an object of this class to a functionexpecting asize_t argument results in an ambiguity as anint and abool may both be used to initialize asize_t.

In the current example, the classString could define the followingconversion operator forchar const *:

    String::operator char const *() const    {        return d_string;    }

Notes:

Conversion operators are also used when objects of classes definingconversion operators are inserted into streams. Realize that the right handsides of insertion operators are function parameters that are initialized bythe operator's right hand side arguments. The rules are simple: In the following example an object of classInsertable is directlyinserted; an object of the classConvertor uses the conversion operator;an object of the classError cannot be inserted since it does not definean insertion operator and the type returned by its conversion operator cannotbe inserted either (Textdoes define anoperator int() const, butthe fact that aText itself cannot be inserted causes the error):
    #include <iostream>    #include <string>    using namespace std;    struct Insertable    {        operator int() const        {            cout << "op int()\n";            return 0;        }    };    ostream &operator<<(ostream &out, Insertable const &ins)    {        return out << "insertion operator";    }    struct Convertor    {        operator Insertable() const        {            return Insertable();        }    };    struct Text    {        operator int() const        {            return 1;        }    };    struct Error    {        operator Text() const        {            return Text{};        }    };    int main()    {        Insertable insertable;        cout << insertable << '\n';        Convertor convertor;        cout << convertor << '\n';        Error error;        cout << error << '\n';    }

Some final remarks regarding conversion operators:

11.4: An alternative implementation of the `byte' type

In chapter3 thestd::byte type was introduced. Itoffers bit-wise and comparison operators but lacks other arithmetic operatorsas well as insertion and extraction operators, which may render it lessuseful. Fortunately, using the facilities provided by operator overloading amore generically useful byte-type can be developed.

In this section we develop a classByte offering all facilities of numerictypes, as well as insertion and extraction operators, while the size of aByte object equals the size of anunsigned char: 1 byte.

A size 1Byte type is realized by defining a classByte having asingleuint8_t data member. Insertion and extraction operators areprovided by free functions. The classByte, therefore, starts as follows:

    #include <iostream>    #include <cstdint>          // uint8_t        class Byte    {        uint8_t d_byte;

All the class's member functions are public:Byte objects can be definedusing the default constructor, the copy constructor, and a constructoraccepting any argument that can be converted to auint8_t:

            Byte();                     // CC available by default            Byte(uint8_t byte);

Byte objects can be used as lvalues, so assignment operators must beprovided. The first (default) assignment operator handles assignment ofByte objects, while the second assignment operator handles assignment ofnumeric types which can be converted touint8_t values:

            Byte &operator=(Byte const &rhs) = default;            Byte &operator=(uint8_t rhs);

Conversion operators are provided soByte objects can be used insituations whereuint8_t values are used:

            operator uint8_t &();            operator uint8_t() const;

Athough conversion operators usually return const references, an excptionis made forByte because of its semantics: as aByte is essentially awrapper around auint8_t a non-const conversion operator is providedallowing the use of arithmetic assignment operators. A statement likebyte+= 13 (having definedByte byte) is compiled asbyte.operator() = 13,which requires the non-const conversion operator.

Most members require a single statement and can very well be implementedinline. Here are their implementations:

    inline Byte::Byte()    :        d_byte(0)    {}        inline Byte::Byte(uint8_t byte)    :        d_byte(byte)    {}        Byte &Byte::operator=(uint8_t rhs)    {        d_byte = rhs;        return *this;    }        inline Byte::operator uint8_t &()    {        return d_byte;    }        inline Byte::operator uint8_t () const // 16_t    {        return d_byte;    }

The insertion and extraction operators act identically to the standardinsertion and extraction operators foruint8_t type values: they insertand extract thechar representation of theByte's value, becausethat's what insertion and extraction operators do: they process text. Here aretheir one-line implementations, using the conversion operators to insert orassign theByte's d_byte value:

    inline std::ostream &operator<<(std::ostream &out, Byte const &byte)    {        return out << byte.operator uint8_t();    }        inline std::istream &operator>>(std::istream &in, Byte &byte)    {        return in >> byte.operator uint8_t &();    }

To write thed_byte's binary value the stream'swrite membershould be used in the way it's always used: reinterpret aByte as achar const * and write a single byte. Reading aByte's binary value isimplemented analogously, using the stream'sread member.

Finally, here are some examples of howByte variables can be used inpractice:

    using namespace std;        int main()    {        Byte b1;            // default: d_byte = 0        Byte b2{ 12 };      // construct from an int        Byte b3{ b2 };      // copy-construct            b1 = 65;            // direct assignment        b1 += 20;           // arithmetic assignment            b1 <<= 1;           // shift-assignments        b1 >>= 1;            b1 |= 1;            // bit-or assignment            uint8_t u8 = b1;    // assign Byte to an uint8_t                                // some stream insertions        cout << sizeof(Byte) << ',' << (b1 < b2) << ',' <<                b1 << ',' << b3 << ',' << u8 << '\n';                                // using 'write'        cout.write(reinterpret_cast<char const *>(&b1), 1) << '\n';    }        // output:    //  hex values                                 text ( . : not printable)    //  31 2C 30 2C 55 2C 0C 2C 55 0A 55 0A        1,0,U,.,U.U.

11.5: The keyword `explicit'

Conversions are not only performed by conversion operators, but also byconstructors accepting one argument (i.e., constructors having one or multipleparameters, specifying default argument values for all parameters or for allbut the first parameter).

Assume a data base classDataBase is defined in whichPersonobjects can be stored. It defines aPerson *d_data pointer, and so itoffers a copy constructor and an overloaded assignment operator.

In addition to the copy constructorDataBase offers a defaultconstructor and several additional constructors:

The above constructors all are perfectly reasonable. But they also allowthe compiler to compile the following code without producing any warning atall:

    DataBase db;    DataBase db2;    Person person;    db2 = db;           // 1    db2 = person;       // 2    db2 = 10;           // 3    db2 = cin;          // 4

Statement 1 is perfectly reasonable:db is used to redefinedb2. Statement 2 might be understandable since we designedDataBase tocontainPerson objects. Nevertheless, we might question the logic that'sused here as aPerson is not some kind ofDataBase. The logic becomeseven more opaque when looking at statements 3 and 4. Statement 3 in effectwaits for the data of 10 persons to appear at the standard inputstream. Nothing like that is suggested bydb2 = 10.

Implicitpromotions are used with statements 2 through 4. Sinceconstructors accepting, respectively aPerson, anistream, and asize_t and anistream have been defined forDataBase and since theassignment operator expects aDataBase right-hand side (rhs) argument thecompiler first converts the rhs arguments to anonymousDataBase objectswhich are then assigned todb2.

It is good practice to prevent implicit promotions by using theexplicit modifier when declaring a constructor. Constructors using theexplicit modifier can only be used to construct objectsexplicitly. Statements 2-4 would not have compiled if the constructorsexpecting one argument would have been declared usingexplicit. E.g.,

    explicit DataBase(Person const &person);    explicit DataBase(size_t count, std:istream &in);

Having declared all constructors accepting one argument asexplicitthe above assignments would have required the explicit specification ofthe appropriate constructors, thus clarifying the programmer's intent:

    DataBase db;    DataBase db2;    Person person;    db2 = db;                   // 1    db2 = DataBase{ person };   // 2    db2 = DataBase{ 10 };       // 3    db2 = DataBase{ cin };      // 4

As arule of thumb prefix one argument constructors with theexplicit keyword unless implicit promotions are perfectly natural(string'schar const * accepting constructor is a case in point).

11.5.1: Explicit conversion operators

In addition to explicit constructors,C++ supportsexplicit conversion operators.

For example, a class might defineoperator bool() const returningtrueif an object of that class is in a usable state andfalse if not.Since the typebool is an arithmetic type this could result in unexpectedor unintended behavior. Consider:

    void process(bool value);    class StreamHandler    {        public:            operator bool() const;      // true: object is fit for use            ...    };    int fun(StreamHandler &sh)    {        int sx;        if (sh)                         // intended use of operator bool()            ... use sh as usual; also use `sx'        process(sh);                    // typo: `sx' was intended    }

In this exampleprocess unintentionally receives the value returned byoperator bool using the implicit conversion frombool toint.

When definingexplicit conversion operators implicit conversions like theone shown in the example are prevented. Such conversion operators can only beused in situations where the converted type is explicitly required (as in thecondition clauses ofif orwhile statements), or is explicitlyrequested using astatic_cast. To declare an explicit bool conversionoperator in classStreamHandler's interface replace the above declarationby:

        explicit operator bool() const;

Since the C++14 standardistreams define anexplicit operator bool() const. As a consequence:

    while (cin.get(ch)) // compiles OK        ;    bool fun1()    {        return cin;     // 'bool = istream' won't compile as     }                   // istream defines 'explicit operator bool'    bool fun1()    {        return static_cast<bool>(cin); // compiles OK    }

11.6: Overloading increment and decrement operators

Overloading theincrement operator(operator++) anddecrement operator (operator--) introduces asmall problem: there are two versions of each operator, as they may be used aspostfix operator (e.g.,x++) or asprefix operator (e.g.,++x).

Used aspostfix operator, the value's object is returned as anrvalue, temporary const object and the post-incremented variable itselfdisappears from view. Used asprefix operator, the variable isincremented, and its value is returned aslvalue and it may be alteredagain by modifying the prefix operator's return value. Whereas thesecharacteristics are notrequired when the operator is overloaded, it isstrongly advised to implement these characteristics in any overloadedincrement or decrement operator.

Suppose we define awrapper class around thesize_t valuetype. Such a class could offer the following (partially shown) interface:

    class Unsigned    {        size_t d_value;        public:            Unsigned();            explicit Unsigned(size_t init);            Unsigned &operator++();    }

The class's last member declares theprefix overloaded incrementoperator. The returnedlvalue isUnsigned &. The member is easilyimplemented:

    Unsigned &Unsigned::operator++()    {        ++d_value;        return *this;    }

To define thepostfix operator, an overloaded version of the operatoris defined, expecting a (dummy)int argument. This might be considered akludge, or an acceptable application of function overloading. Whateveryour opinion in this matter, the following can be concluded:

The postfix increment operator is declared as follows in the classUnsigned's interface:
    Unsigned operator++(int);

It may be implemented as follows:

    Unsigned Unsigned::operator++(int)    {        Unsigned tmp{ *this };        ++d_value;        return tmp;    }

Note that the operator's parameter is not used. It is only part of theimplementation todisambiguate the prefix- and postfix operators inimplementations and declarations.

In the above example the statement incrementing the current object offersthenothrow guarantee as it only involves an operation on aprimitive type. If the initial copy construction throws then the originalobject is not modified, if the return statement throws the object has safelybeen modified. But incrementing an object could itself throw exceptions. Howto implement the increment operators in that case? Once again,swap is ourfriend. Here are the pre- and postfix operators offering the strong guaranteewhen the memberincrement performing the increment operation may throw:

    Unsigned &Unsigned::operator++()    {        Unsigned tmp{ *this };        tmp.increment();        swap(tmp);        return *this;    }    Unsigned Unsigned::operator++(int)    {        Unsigned tmp{ *this };        tmp.increment();        swap(tmp);        return tmp;    }

Both operators first create copies of the current objects. These copiesare incremented and then swapped with the current objects. Ifincrementthrows the current objects remain unaltered; the swap operations ensure thatthe correct objects are returned (the incremented object for the prefixoperator, the original object for the postfix operator) and that the currentobjects become the incremented objects.

When calling the increment or decrement operator using its full memberfunction name then anyint argument passed to the function results incalling the postfix operator. Omitting the argument results in calling theprefix operator. Example:

    Unsigned uns{ 13 };    uns.operator++();     // prefix-incrementing uns    uns.operator++(0);    // postfix-incrementing uns

Both the prefix and postfix increment and decrement operators are deprecatedwhen applied tobool type of variables. In situations where a postfixincrement operator could be useful thestd::exchange (cf. section19.1.13) should be used.

11.7: Overloading binary operators

In various classes overloading binary operators (likeoperator+) canbe a very natural extension of the class's functionality. For example, thestd::string class has various overloadedoperator+ members.

Most binary operators come in two flavors: the plain binary operator (likethe+ operator) and the compound binary assignment operator (likeoperator+=). Whereas the plain binary operators return values, thecompound binary assignment operators usually return references to the objectsfor which the operators were called. For example, withstd::string objectsthe following code (annotations below the example) may be used:

    std::string s1;    std::string s2;    std::string s3;    s1 = s2 += s3;                  // 1    (s2 += s3) + " postfix";        // 2    s1 = "prefix " + s3;            // 3    "prefix " + s3 + "postfix";     // 4

Now consider the following code, in which a classBinary supports anoverloadedoperator+:

    class Binary    {        public:            Binary();            Binary(int value);            Binary operator+(Binary const &rhs);    };    int main()    {        Binary b1;        Binary b2{ 5 };        b1 = b2 + 3;            // 1        b1 = 3 + b2;            // 2    }
Compilation of this little program fails for statement// 2, with thecompiler reporting an error like:
    error: no match for 'operator+' in '3 + b2'

Why is statement// 1 compiled correctly whereas statement// 2won't compile?

In order to understand this rememberpromotions. As we have seen insection11.5, constructors expecting single arguments may implicitlybe activated when an argument of an appropriate type is provided. We'vealready encountered this withstd::string objects, where NTBSs may be usedto initializestd::string objects.

Analogously, in statement// 1,operator+ is called, usingb2as its left-hand side operand. This operator expects anotherBinary objectas its right-hand side operand. However, anint is provided. But as aconstructorBinary(int) exists, theint value can be promoted to aBinary object. Next, thisBinary object is passed as argument to theoperator+ member.

Unfortunately, in statement// 2 promotions are not available: herethe+ operator is applied to anint-type lvalue. Anint is aprimitive type and primitive types have no knowledge of `constructors',`member functions' or `promotions'.

How, then, are promotions of left-hand operands implemented in statementslike"prefix " + s3? Since promotions can be applied to functionarguments, we must make sure that both operands of binary operators arearguments. This implies that plain binary operators supporting promotions foreither their left-hand side operand or right-hand side operand should bedeclared asfree operators, also calledfree functions.

Functions like the plain binary operators conceptually belong to the classfor which they implement these operators. Consequently they should bedeclared in the class's header file. We cover their implementationsshortly, but here is our first revision of the declaration of the classBinary, declaring an overloaded+ operator as a free function:

    class Binary    {        public:            Binary();            Binary(int value);    };    Binary operator+(Binary const &lhs, Binary const &rhs);

After defining binary operators as free functions, several promotions areavailable:

The next step consists of implementing the required overloaded binarycompound assignment operators, having the form@=, where@ representsa binary operator. As these operatorsalways have left-hand side operandswhich are object of their own classes, they are implemented as genuine memberfunctions. Compound assignment operators usually return references to theobjects for which the binary compound assignment operators were requested, asthese objects might be modified in the same statement. E.g.,(s2 += s3) + " postfix".

Here is our second revision of the classBinary, showing thedeclaration of the plain binary operator as well as the corresponding compoundassignment operator:

    class Binary    {        public:            Binary();            Binary(int value);            Binary &operator+=(Binary const &rhs);    };    Binary operator+(Binary const &lhs, Binary const &rhs);

How should the compound addition assignment operator be implemented? Whenimplementing compound binary assignment operators the strong guarantee shouldalways be kept in mind: if the operation might throw use a temporary objectand swap. Here is our implementation of the compound assignment operator:

    Binary &Binary::operator+=(Binary const &rhs)    {        Binary tmp{ *this };        tmp.add(rhs);           // this might throw        swap(tmp);        return *this;    }

It's easy to implement the free binary operator: thelhs argument iscopied into aBinary tmp to which therhs operand is added. Thentmp is returned, using copy elision. The classBinary declares thefree binary operator as a friend (cf. chapter15), so it can callBinary's add member:

    class Binary    {        friend Binary operator+(Binary const &lhs, Binary const &rhs);        public:            Binary();            Binary(int value);            Binary &operator+=(Binary const &other);        private:            void add(Binary const &other);    };

The binary operator's implementation becomes:

    Binary operator+(Binary const &lhs, Binary const &rhs)    {        Binary tmp{ lhs };        tmp.add(rhs);        return tmp;    }

If the classBinary is move-aware then it's attractive to add move-awarebinary operators. In this case we also need operators whose left-hand sideoperands are rvalue references. When a class is move aware various interestingimplementations are suddenly possible, which we encounter below, and inthe next (sub)section. First have a look at the signature of such a binaryoperator (which should also be declared as a friend in the class interface):

    Binary operator+(Binary &&lhs, Binary const &rhs);

Since the lhs operand is an rvalue reference, we can modify itad lib. Binary operators are commonly designed as factory functions, returning objectscreated by those operators. However, the (modified) object referred to bylhs should itselfnot be returned. As stated in the C++ standard,

A temporary object bound to a reference parameter in a function call persists until the completion of the full-expression containing the call.
and furthermore:
The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed at the end of the full-expression in the return statement.
In other words, a temporary object cannot itself be returned as thefunction's return value: aBinary && return type should therefore not beused. Therefore functions implementing binary operators are factory functions(note, however, that the returned object may be constructed using the class'smove constructor whenever a temporary object has to be returned).

Alternatively, the binary operator can first create an object by moveconstructing it from the operator's lhs operand, performing the binaryoperation on that object and the operator's rhs operand, and then return themodified object (allowing the compiler to apply copy elision). It's a matterof taste which one is preferred.

Here are the two implementations. Because of copy elision the explicitlydefinedret object is created in the location of the return value. Bothimplementations, although they appear to be different, show identical run-timebehavior:

                // first implementation: modify lhs    Binary operator+(Binary &&lhs, Binary const &rhs)       {        lhs.add(rhs);        return std::move(lhs);    }                // second implementation: move construct ret from lhs    Binary operator+(Binary &&lhs, Binary const &rhs)       {        Binary ret{ std::move(lhs) };        ret.add(rhs);        return ret;    }

Now, when executing expressions like (allBinary objects)b1 + b2 + b3 the following functions are called:

    copy operator+          = b1 + b2     Copy constructor        = tmp(b1)         adding              = tmp.add(b2)    copy elision            : tmp is returned from b1 + b2            move operator+          = tmp + b3     adding                  = tmp.add(b3)    Move construction       = tmp2(move(tmp)) is returned

But we're not there yet: in the next section we encounter possibilitiesfor several more interesting implementations, in the context of compoundassignment operators.

11.7.1: Member function reference bindings (& and &&)

We've seen that binary operators (likeoperator+) can be implemented veryefficiently, but require at least move constructors.

An expression like

    Binary{} + varB + varC + varD

therefore returns a move constructed object representingBinary{} +varB, then another move constructed object receiving the first return valueandvarC, and finally yet another move constructed object receiving thesecond returned object andvarD as its arguments.

Now consider the situation where we have a function defining aBinary &&parameter, and a secondBinary const & parameter. Inside that functionthese values need to be added, and their sum is then passed as argument to twoother functions. Wecould do this:

    void fun1(Binary &&lhs, Binary const &rhs)    {        lhs += rhs;        fun2(lhs);        fun3(lhs);    }

But realize that when usingoperator+= we first construct a copy ofthe current object, so a temporary object is available to perform the additionon, and then swap the temporary object with the current object to commit theresults. But wait! Our lhs operand alreadyis a temporary object. So whycreate another?

In this example another temporary object is indeed not required:lhsremains in existence untilfun1 ends. But different from the binaryoperators the binary compound assignment operators don't have explicitlydefined left-hand side operands. But we still can inform the compiler that aparticularmember (so, not merely compound assignment operators) shouldonly be used when the objects calling those members is an anonymous temporaryobject, or a non-anonymous (modifiable or non-modifiable) object. For thiswe usereference bindings a.k.a.reference qualifiers.

Reference bindings consist of a reference token (&), optionallypreceded byconst, or an rvalue reference token (&&). Such referencequalifiers are immediately affixed to the function's head (this applies to thedeclaration and the implementation alike). Functions provided with rvaluereference bindings are selected by the compiler when used by anonymoustemporary objects, whereas functions provided with lvalue reference bindingsare selected by the compiler when used by other types of objects.

Reference qualifiers allow us to fine-tune our implementations of compoundassignment operators likeoperator+=. If we know that the object callingthe compound assignment operator is itself a temporary, then there's no needfor a separate temporary object. The operator may directly perform itsoperation and could then return itself as an rvalue reference. Here is theimplementation ofoperator+= tailored to being used by temporary objects:

    Binary &&Binary::operator+=(Binary const &rhs) &&    {        add(rhs);                   // directly add rhs to *this,         return std::move(*this);    // return the temporary object itself    }

This implementation is about as fast as it gets. But be careful: in theprevious section we learned that a temporary is destroyed at the end of thefull expression of a return stattement. In this case, however, the temporaryalready exists, and so (also see the previous section) it should persist untilthe expression containing the (operator+=) function call is completed. Asa consequence,

    cout << (Binary{} += existingBinary) << '\n';

is OK, but

    Binary &&rref = (Binary{} += existingBinary);    cout << rref << '\n';

is not, sincerref becomes a dangling reference immediately after itsinitialization.

A full-proof alternative implementation of the rvalue-reference boundoperator+= returns a move-constructed copy:

    Binary Binary::operator+=(Binary const &rhs) &&    {        add(rhs);                   // directly add rhs to *this,         return std::move(*this);    // return a move constructed copy    }

The price to pay for this full-proof implementation is an extra moveconstruction. Now, using the previous example (usingrref),operator+=returns a copy of theBinary{} temporary, which is still a temporaryobject which can safely be referred to byrref.

Which implementation to use may be a matter of choice: if users ofBinary know what they're doing then the former implementation can be used,since these users will never use the aboverref initialization. If you'renot so sure about your users, use the latter implementation: formally yourusers will do something they shouldn't do, but there's no penalty for that.

For the compound assignment operator called by an lvalue reference (i.e.,a named object) we use the implementation foroperator+= from the previoussection (note the reference qualifier):

    Binary &Binary::operator+=(Binary const &rhs) &    {        Binary tmp(*this);        tmp.add(rhs);           // this might throw        swap(tmp);        return *this;    }

With this implementation addingBinary objects to each other(e.g.,b1 += b2 += b3) boils down to

    operator+=    (&)       = b2 += b3    Copy constructor        = tmp(b2)         adding              = tmp.add(b3)        swap                = b2 <-> tmp    return                  = b2    operator+=    (&)       = b1 += b2    Copy constructor        = tmp(b1)         adding              = tmp.add(b2)        swap                = b1 <-> tmp    return                  = b1

When the leftmost object is a temporary then a copy construction and swapcall are replaced by the construction of an anonymous object. E.g.,withBinary{} += b2 += b3 we observe:

    operator+=    (&)       = b2 += b3    Copy constructor        = tmp(b2)         adding              = tmp.add(b3)        swap                = b2 <-> tmp        Anonymous object        = Binary{}    operator+=    (&&)      = Binary{} += b2        adding              = add(b2)    return                  = move(Binary{})

ForBinary &Binary::operator+=(Binary const &rhs) & an alternativeimplementation exists, merely using a single return statement, but in factrequiring two extra function calls. It's a matter of taste whether you preferwriting less code or executing fewer function calls:

    Binary &Binary::operator+=(Binary const &rhs) &    {        return *this = Binary{ *this } += rhs;    }

Notice that the implementations ofoperator+ andoperator+= areindependent of the actual definition of the classBinary. Adding standardbinary operators to a class (i.e., operators operating on arguments of theirown class types) can therefore easily be realized.

11.7.2: The three-way comparison operator `<=>'

TheC++26 standard added thethree-way comparison operator<=>,also known as thespaceship operator, to the language.

This operator is closely related to comparison classes, coveredin section18.7. At this point we focus on using thestd::strong_ordering class: the examples of the spaceship operatorpresented in this section all returnstrong_ordering objects. Theseobjects are

Standard operand conversions are handled by the compiler. Note that

Other standard conversions, like lvalue transformations and qualificationconversions (cf. section21.4), are automatically performed.

Now about the spaceship operator itself. Why would you want it? Of course, ifit's defined then you can use it. As it's available for integral numeric typesthe following correctly compiles:

    auto isp =    3 <=> 4;
whereafterisp's value can be compared to availableoutcome-values:
    cout << ( isp == strong_ordering::less ? "less\n" : "not less\n" );

But that by itself doesn't make the spaceship operator all toointeresting. Whatdoes make it interesting is that, in combination withoperator==, it handlesall comparison operators. So after providing aclass withoperator== andoperator<=> its objects can be compared forequality, inequality, and they can be ordered by<, <=, >, and>=. Asan example consider books. To book owners the titles and author names are thebooks' important characterstics. To sort them on book shelfs we must useoperator<, to find a particular book we useoperator==, to determinewhether two books are different we useoperator!= and if you want to orderthem in an country where Arabic is the main language you might want to sortthem usingoperator> considering that the prevalent reading order in thosecountries is from right to left. Ignoring constructors, destructors and othermembers, then this is the interface of our classBook (note the inclusionof the<compare> header file, containing the declarations of thecomparison classes):

    #include <string>    #include <compare>    class Book    {        friend bool operator==(Book const &lhs, Book const &rhs);        friend std::strong_ordering operator<=>(Book const &lhs,                                                 Book const &rhs);        std::string d_author;        std::string d_title;        // ...    };
Both friend-functions are easy to implement:
    bool operator==(Book const &lhs, Book const &rhs)    {        return lhs.d_author == rhs.d_author and lhs.d_title == rhs.d_title;    }        strong_ordering operator<=>(Book const &lhs, Book const &rhs)    {        return lhs.d_author < rhs.d_author  ? strong_ordering::less     :               lhs.d_author > rhs.d_author  ? strong_ordering::greater  :               lhs.d_title  < rhs.d_title   ? strong_ordering::less     :               lhs.d_title  > rhs.d_title   ? strong_ordering::greater  :                                              strong_ordering::equal;    }
And that's it! Now all comparison operators (and of course the spaceshipoperator itself) are available. The following now compiles flawlessly:
    void books(Book const &b1, Book const &b2)    {        cout << (b1 == b2) << (b1 != b2) << (b1 <  b2) <<                 (b1 <= b2) << (b1 >  b2) << (b1 >= b2) << '\n';    }
callingbooks for two identical books inserts 100101 intocout.

The spaceship operator is available for integral numeric types and may havebeen defined for class types. E.g., it is defined forstd::string. It isnot automatically available for floating point types.

11.8: Overloading `operator new(size_t)'

Whenoperator new is overloaded, it must define avoid * returntype, and its first parameter must be of typesize_t. The defaultoperator new defines only one parameter, but overloaded versions maydefine multiple parameters. The first one is not explicitly specified but isdeduced from the size of objects of the class for whichoperator newis overloaded. In this section overloadingoperator new is discussed.Overloadingnew[] is discussed in section11.10.

It is possible to define multiple versions of theoperator new, as long aseach version defines its own unique set of arguments. When overloadedoperator new members must dynamically allocate memory they can do so usingthe globaloperator new, applying the scope resolution operator::. In the next example the overloadedoperator new of the classString initializes the substrate of dynamically allocatedStringobjects to 0-bytes:

    #include <cstring>    #include <iosfwd>    class String    {        std::string *d_data;        public:            void *operator new(size_t size)            {                return memset(::operator new(size), 0, size);            }            bool empty() const            {                return d_data == 0;            }    };

The aboveoperator new is used in the following program, illustratingthat even thoughString's default constructor does nothing the object'sdata memberd_data is initialized to zero:

    #include "string.h"    #include <iostream>    using namespace std;    int main()    {        String *sp = new String;        cout << boolalpha << sp->empty() << '\n';   // shows: true    }

Atnew String the following took place:

AsString::operator new initialized the allocated memory to zero bytesthe allocatedString object'sd_data member had already beeninitialized to a 0-pointer by the time it started to exist.

All member functions (including constructors and destructors) we'veencountered so far define a (hidden) pointer to the object on which theyshould operate. This hidden pointer becomes the function'sthis pointer.

In the next example ofpseudoC++code, the pointer is explicitlyshown to illustrate what's happening whenoperator new is used. In thefirst part aString objectstr is directly defined, in the secondpart of the example the (overloaded)operator new is used:

    String::String(String *const this);     // real prototype of the default                                            // constructor    String *sp = new String;                // This statement is implemented                                            // as follows:    String *sp = static_cast<String *>(            // allocation                        String::operator new(sizeof(String))                 );    String::String{ sp };                          // initialization

In the above fragment the member functions were treated asobject-less member functions of the classString. Such members arecalledstatic member functions (cf. chapter8). Actually,operator newis such a static member function. Since it has nothis pointer it cannot reach data members of the object for which it isexpected to make memory available. It can only allocate and initialize theallocated memory, but cannot reach the object's data members by name as thereis as yet no data object layout defined.

Following the allocation, the memory is passed (as thethis pointer)to the constructor for further processing.

Operator new can have multiple parameters. The first parameter isinitialized as an implicit argument and is always asize_tparameter. Additional overloaded operators may define additionalparameters. An interesting additionaloperator new is theplacement new operator. With the placement newoperator a block of memory has already been set aside and one of the class'sconstructors is used to initialize that memory. Overloading placement newrequires anoperator new having two parameters:size_t andchar *,pointing to the memory that was already available. Thesize_tparameter is implicitly initialized, but the remaining parameters mustexplicitly be initialized using arguments tooperator new. Hence we reachthe familiar syntactical form of the placement new operator in use:

    char buffer[sizeof(String)];        // predefined memory    String *sp = new(buffer) String;    // placement new call

The declaration of the placement new operator in our classStringlooks like this:

    void *operator new(size_t size, char *memory);

It could be implemented like this (also initializing theString'smemory to 0-bytes):

    void *String::operator new(size_t size, char *memory)    {        return memset(memory, 0, size);    }

Any other overloaded version ofoperator new could also bedefined. Here is an example showing the use and definition of an overloadedoperator new storing the object's address immediately in an existing arrayof pointers toString objects (assuming the array is large enough):

        // use:    String *next(String **pointers, size_t *idx)    {        return new(pointers, (*idx)++) String;    }        // implementation:    void *String::operator new(size_t size, String **pointers, size_t idx)    {        return pointers[idx] = ::operator new(size);    }

11.9: Overloading `operator delete(void *)'

Thedelete operator may also be overloaded. In fact it's good practice tooverloadoperator delete wheneveroperator new is also overloaded.

Operator delete must define avoid * parameter. A second overloadedversion defining a second parameter of typesize_t is related tooverloadingoperator new[] and is discussed in section11.10.

Overloadedoperator delete members returnvoid.

The `home-made'operator delete is called when deleting a dynamicallyallocated object after executing the destructor of the associatedclass. So, the statement

    delete ptr;

withptr being a pointer to an object of the classString forwhich the operatordelete was overloaded, is a shorthand for the followingstatements:

    ptr->~String(); // call the class's destructor                    // and do things with the memory pointed to by ptr    String::operator delete(ptr);

The overloadedoperator delete may do whatever it wants to do with thememory pointed to byptr. It could, e.g., simply delete it. If that wouldbe the preferred thing to do, then the defaultdelete operator can be called using the::scope resolution operator. For example:

    void String::operator delete(void *ptr)    {        // any operation considered necessary, then, maybe:        ::delete ptr;    }

To declare the above overloadedoperator delete simply add thefollowing line to the class's interface:

    void operator delete(void *ptr);

Likeoperator new operator delete is a static member function(see also chapter8).

11.10: Operators `new[]' and `delete[]'

In sections9.1.1,9.1.2 and9.2.1operator new[] andoperator delete[] were introduced. Likeoperator new andoperator delete theoperatorsnew[] anddelete[] may be overloaded.

As it is possible to overloadnew[] anddelete[] as well asoperator new andoperator delete, one should be careful in selectingthe appropriate set of operators. The followingrule of thumb should alwaysbe applied:

Ifnew is used toallocate memory,delete should be used todeallocate memory. Ifnew[] is used to allocate memory,delete[] should be used to deallocate memory.

By default these operators act as follows:

11.10.1: Overloading `new[]'

To overloadoperator new[] in a class (e.g., in the classString) add the following line to the class's interface:
    void *operator new[](size_t size);

The member'ssize parameter is implicitly provided and is initializedbyC++'s run-time system to the amount of memory that must be allocated.Like the simple one-objectoperator new itshould return avoid *. The number of objects that must be initialized caneasily be computed fromsize / sizeof(String) (and of course replacingString by the appropriate class name when overloadingoperator new[]for another class). The overloadednew[] member may allocate raw memoryusing e.g., the defaultoperator new[] or the defaultoperator new:

    void *operator new[](size_t size)    {        return ::operator new[](size);        // alternatively:        // return ::operator new(size);    }

Before returning the allocated memory the overloadedoperator new[]has a chance to do something special. It could, e.g., initialize the memory tozero-bytes.

Once the overloadedoperator new[] has been defined, it isautomatically used in statements like:

    String *op = new String[12];

Likeoperator new additional overloads ofoperator new[] may bedefined. One opportunity for anoperator new[] overload is overloadingplacement new specifically for arrays of objects. This operator isavailable by default but becomes unavailable once at least one overloadedoperator new[] is defined. Implementing placementnew is notdifficult. Here is an example, initializing the available memory to 0-bytesbefore returning:

    void *String::operator new[](size_t size, char *memory)    {        return memset(memory, 0, size);    }

To use this overloaded operator, the second parameter must again beprovided, as in:

    char buffer[12 * sizeof(String)];    String *sp = new(buffer) String[12];

11.10.2: Overloading `delete[]'

To overloadoperator delete[] in a classString add the following lineto the class's interface:
    void operator delete[](void *memory);

Its parameter is initialized to the address of a block of memorypreviously allocated byString::new[].

There are some subtleties to be aware of when implementingoperator delete[]. Although the addresses returned bynew andnew[] point to the allocated object(s), there is an additionalsize_tvalue available immediately before the address returned bynew andnew[]. Thissize_t value is part of the allocated block and containsthe actual size of the block. This of course does not hold true for theplacementnew operator.

When a class defines a destructor thesize_t value preceding theaddress returned bynew[] does not contain the size of the allocatedblock, but thenumber of objects specified when callingnew[]. Normally that is of no interest, but when overloadingoperatordelete[] it might become a useful piece of information. In those casesoperator delete[] doesnot receive the address returned bynew[]but rather the address of the initialsize_t value. Whether this is at alluseful is not clear. By the timedelete[]'s code is executed all objectshave already been destroyed, sooperator delete[] is only to determine howmany objects were destroyed but the objects themselves cannot be used anymore.

Here is an example showing this behavior ofoperator delete[] for aminimalDemo class:

    struct Demo    {        size_t idx;        Demo()        {            cout << "default cons\n";        }        ~Demo()        {            cout << "destructor\n";        }        void *operator new[](size_t size)        {            return ::operator new(size);        }        void operator delete[](void *vp)        {            cout << "delete[] for: " << vp << '\n';            ::operator delete[](vp);        }    };    int main()    {        Demo *xp;        cout << ((int *)(xp = new Demo[3]))[-1] << '\n';        cout << xp << '\n';        cout << "==================\n";        delete[] xp;    }    // This program displays (your 0x?????? addresses might differ, but    // the difference between the two should be sizeof(size_t)):    //  default cons    //  default cons    //  default cons    //  3    //  0x8bdd00c    //  ==================    //  destructor    //  destructor    //  destructor    //  delete[] for: 0x8bdd008

Having overloadedoperator delete[] for a classString, it will beused automatically in statements like:

        delete[] new String[5];

Operatordelete[] may also be overloaded using an additionalsize_t parameter:

    void operator delete[](void *p, size_t size);

Heresize is automatically initialized to the size (in bytes) of theblock of memory to whichvoid *p points. If this form is defined, thenvoid operator[](void *) shouldnot be defined, to avoid ambiguities.An example of this latter form ofoperator delete[] is:

    void String::operator delete[](void *ptr, size_t size)    {        cout << "deleting " << size << " bytes\n";        ::operator delete[](ptr);    }

Additional overloads ofoperator delete[] may be defined, but to usethem they must explicitly be called as static member functions (cf. chapter8). Example:

        // declaration:    void String::operator delete[](void *ptr, ostream &out);        // usage:    String *xp = new String[3];    String::operator delete[](xp, cout);

11.10.3: The `operator delete(void *, size_t)' family

As we've seen classes may overload theiroperator delete andoperatordelete[] members.

Since the C++14 standard the globalvoid operator delete(void *, size_tsize) andvoid operator delete[](void *, size_t size) functions can alsobe overloaded.

When a globalsized deallocation function is defined, it is automaticallyused instead of the default, non-sized deallocation function. The performanceof programs may improve if a sized deallocation function is available (cf.http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3663.html).

11.10.4: `new[]', `delete[]' and exceptions

When an exception is thrown while executing anew[] expression, what willhappen? In this section we'll show thatnew[] is exception safe even whenonly some of the objects were properly constructed.

To begin,new[] might throw while trying to allocate the requiredmemory. In this case abad_alloc is thrown and we don't leak as nothingwas allocated.

Having allocated the required memory the class's default constructor is goingto be used for each of the objects in turn. At some point a constructor mightthrow. What happens next is defined by theC++ standard: the destructorsof the already constructed objects are called and the memory allocated forthe objects themselves is returned to the common pool. Assuming that thefailing constructor offers the basic guaranteenew[] is thereforeexception safe even if a constructor may throw.

The following example illustrates this behavior. A request to allocate andinitialize five objects is made, but after constructing two objectsconstruction fails by throwing an exception. The output shows that thedestructors of properly constructed objects are called and that the allocatedsubstrate memory is properly returned:

    #include <iostream>    using namespace std;    static size_t count = 0;    class X    {        int x;        public:            X()            {                if (count == 2)                    throw 1;                cout << "Object " << ++count << '\n';            }            ~X()            {                cout << "Destroyed " << this << "\n";            }            void *operator new[](size_t size)            {                cout << "Allocating objects: " << size << " bytes\n";                return ::operator new(size);            }            void operator delete[](void *mem)            {                cout << "Deleting memory at " << mem << ", containing: " <<                    *static_cast<int *>(mem) << "\n";                ::operator delete(mem);            }    };    int main()    try    {        X *xp = new X[5];        cout << "Memory at " << xp << '\n';        delete[] xp;    }    catch (...)    {        cout << "Caught exception.\n";    }    // Output from this program (your 0x??? addresses might differ)    //  Allocating objects: 24 bytes    //  Object 1    //  Object 2    //  Destroyed 0x8428010    //  Destroyed 0x842800c    //  Deleting memory at 0x8428008, containing: 5    //  Caught exception.

11.11: Function Objects

Function Objects are created by overloading thefunction call operatoroperator(). By defining the function calloperator an object masquerades as a function, hence the termfunction objects. Function objects are also known asfunctors.

Function objects are important when usinggeneric algorithms. The use of function objects ispreferred over alternatives like pointers to functions. The fact that they areimportant in the context of generic algorithms leaves us with a didacticdilemma. At this point in theC++ Annotations it would have been nice if genericalgorithms would already have been covered, but for the discussion of thegeneric algorithms knowledge of function objects is required. Thisbootstrapping problem is solved in a well-known way: by ignoring thedependency for the time being, for now concentrating on the function objectconcept.

Function objects are objects for whichoperator() has beendefined. Function objects are not just used in combination with genericalgorithms, but also as a (preferred) alternative to pointers tofunctions.

Function objects are frequently used to implementpredicate functions. Predicate functions return boolean values.Predicate functions and predicate function objects are commonly referred to as`predicates'. Predicates are frequently used by generic algorithms such as thecount_if generic algorithm, covered in chapter19,returning the number of times its function object has returnedtrue. Inthestandard template library two kinds of predicates are used:unarypredicates receive one argument,binary predicates receive two arguments.

Assume we have a classPerson and an array ofPerson objects. Furtherassume that the array is not sorted. A well-known procedure for finding aparticularPerson object in the array is to use the functionlsearch, which performs alinear search in an array. Example:

    Person &target = targetPerson();    // determine the person to find    Person *pArray;    size_t n = fillPerson(&pArray);    cout << "The target person is";    if (not lsearch(&target, pArray, &n, sizeof(Person), compareFunction))        cout << " not";    cout << " found\n";

The functiontargetPerson determines the person we're looking for, andfillPerson is called to fill the array. Thenlsearch is used tolocate the target person.

The comparison function must be available, as its address is one of thearguments oflsearch. It must be a real function having an address. If itis defined inline then the compiler has no choice but to ignore that requestas inline functions don't have addresses.CompareFunction could beimplemented like this:

    int compareFunction(void const *p1, void const *p2)    {        return *static_cast<Person const *>(p1)     // lsearch wants 0                !=                                  // for equal objects                *static_cast<Person const *>(p2);    }

This, of course, assumes that theoperator!= has been overloaded inthe classPerson. But overloadingoperator!= is no big deal, solet's assume that that operator is actually available.

On averagen / 2 timesat least thefollowing actions take place:

  1. The two arguments of the compare function are pushed on the stack;
  2. The value of the final parameter oflsearch is determined,producingcompareFunction's address;
  3. The compare function is called;
  4. Then, inside the compare function the address of the right-handargument of thePerson::operator!= argument is pushed on the stack;
  5. Person::operator!= is evaluated;
  6. The argument of thePerson::operator!= function is popped offthe stack;
  7. The two arguments of the compare function are popped off the stack.
Using function objects results in a different picture. Assume we haveconstructed a functionPersonSearch, having the following prototype (this,however, is not the preferred approach. Normally a generic algorithm ispreferred over a home-made function. But for now we focus onPersonSearchto illustrate the use and implementation of a function object):
    Person const *PersonSearch(Person *base, size_t nmemb,                               Person const &target);

This function can be used as follows:

    Person &target = targetPerson();    Person *pArray;    size_t n = fillPerson(&pArray);    cout << "The target person is";    if (!PersonSearch(pArray, n, target))        cout << " not";    cout << "found\n";

So far, not much has been changed. We've replaced the call tolsearchwith a call to another function:PersonSearch. Now look atPersonSearch itself:

    Person const *PersonSearch(Person *base, size_t nmemb,                                Person const &target)    {        for (int idx = 0; idx < nmemb; ++idx)            if (target(base[idx]))                return base + idx;        return 0;    }

PersonSearch implements a plainlinear search. However, in thefor-loop we seetarget(base[idx]). Heretarget is used as afunction object. Its implementation is simple:

    bool Person::operator()(Person const &other) const    {        return *this == other;    }

Note the somewhatpeculiar syntax:operator(). The first set ofparentheses define the operator that is overloaded: the function calloperator. The second set of parentheses define the parameters that arerequired for this overloaded operator. In the class header file thisoverloaded operator is declared as:

    bool operator()(Person const &other) const;

ClearlyPerson::operator() is a simple function. It contains but onestatement, and we could consider defining itinline. Assuming we do,then this is what happens whenoperator() is called:

  1. The address of the right-hand argument of thePerson::operator== argument is pushed on the stack;
  2. Theoperator== function is evaluated (which probably also is asemantic improvement over callingoperator!= when looking for anobjectequal to a specified target object);
  3. The argument ofPerson::operator== argument is popped off the stack.
Due to the fact thatoperator() is an inline function, it is notactually called. Insteadoperator== is called immediately. Moreover, therequiredstack operations are fairly modest.

Function objects may truly be defined inline. Functions that are calledindirectly (i.e., using pointers to functions) can never be defined inline astheir addresses must be known. Therefore, even if the function object needsto do very little work it is defined as an ordinary function if it is going tobe called through pointers. The overhead of performing the indirect call mayannihilate the advantage of the flexibility of calling functionsindirectly. In these cases using inline function objects can result in anincrease of a program's efficiency.

An added benefit of function objects is that they may access the private dataof their objects. In a search algorithm where a compare function is used (aswithlsearch) the target and array elements are passed to the comparefunction using pointers, involving extra stack handling. Using functionobjects, the target person doesn't vary within a single searchtask. Therefore, the target person could be passed to the function object'sclass constructor. This is in fact what happens in the expressiontarget(base[idx]) receiving as its only argument the subsequent elementsof the array to search.

11.11.1: Constructing manipulators

In chapter6 we saw constructions likecout <<hex<<13 << to display the value 13 in hexadecimal format. Onemay wonder by what magic thehexmanipulator accomplishes this. In thissection the construction of manipulators likehex is covered.

Actually the construction of a manipulator is rather simple. To start, adefinition of the manipulator is needed. Let's assume we want to create amanipulatorw10 which sets thefield width of the next field to bewritten by theostream object to 10. This manipulator is constructed as afunction. Thew10 function needs to know about theostream objectin which the width must be set. By providing the function with anostream& parameter, it obtains this knowledge. Now that the function knows about theostream object we're referring to, it can set the width in that object.

Next, it must be possible to use the manipulator in an insertionsequence. This implies that thereturn value of the manipulator must beareference to anostream object also.

From the above considerations we're now able to construct ourw10function:

    #include <ostream>    #include <iomanip>    std::ostream &w10(std::ostream &str)    {        return str << std::setw(10);    }
Thew10 function can of course be used in a `stand alone' mode, but itcan also be used as a manipulator. E.g.,
        #include <iostream>        #include <iomanip>        using namespace std;        extern ostream &w10(ostream &str);        int main()        {            w10(cout) << 3 << " ships sailed to America\n";            cout << "And " << w10 << 3 << " more ships sailed too.\n";        }
Thew10 function can be used as a manipulator because theclassostream has an overloadedoperator<< accepting a pointer to a function expecting anostream &and returning anostream &. Its definition is:
    ostream &operator<<(ostream &(*func)(ostream &str))    {        return (*func)(*this);    }
E.g., thisstd::ostream &(*func)(std::ostream &str) function is thesignature of thestd::endl manipulator.

In addition to the above overloadedoperator<< another one is defined

    ios_base &operator<<(ios_base &(*func)(ios_base &base))    {        (*func)(*this);        return *this;    }

This latter function is used when inserting, e.g.,hex orinternal.

The above procedure does not work for manipulators requiring arguments.It is of course possible to overloadoperator<< to accept anostreamreference and the address of a function expecting anostream & and, e.g.,anint, but while the address of such a function may be specified with the<<-operator, the arguments itself cannot be specified. So, one wondershow the following construction has been implemented:

    cout << setprecision(3)

In this case the manipulator is defined as amacro. Macro's, however,are the realm of thepreprocessor, and may easily suffer from unwelcomeside-effects. InC++ programs they should be avoided wheneverpossible. The following section introduces a way to implement manipulatorsrequiring arguments without resorting to macros, but using anonymous objects.

11.11.1.1: Manipulators requiring arguments

Manipulators taking arguments are implemented as macros: they arehandled by thepreprocessor, and are not available beyond the preprocessingstage.

Manipulators, maybe requiring arguments, can also be defined without usingmacros. One solution, suitable for modifying globally available objects (likecin, orcout) is based on using anonymousobjects:

Here is an example of a little program using such ahome-mademanipulator expecting multiple arguments:
    #include <iostream>    #include <iomanip>    class Align    {        unsigned d_width;        std::ios::fmtflags d_alignment;        public:            Align(unsigned width, std::ios::fmtflags alignment);            std::ostream &operator()(std::ostream &ostr) const;    };    Align::Align(unsigned width, std::ios::fmtflags alignment)    :        d_width(width),        d_alignment(alignment)    {}    std::ostream &Align::operator()(std::ostream &ostr) const    {        ostr.setf(d_alignment, std::ios::adjustfield);        return ostr << std::setw(d_width);    }    std::ostream &operator<<(std::ostream &ostr, Align &&align)    {        return align(ostr);    }    using namespace std;    int main()    {        cout            << "`" << Align{ 5, ios::left } << "hi" << "'"            << "`" << Align{ 10, ios::right } << "there" << "'\n";    }    /*        Generated output:        `hi   '`     there'    */

When (local) objects must be manipulated, then the class that must providemanipulators may define function call operators receiving the requiredarguments. E.g., consider a classMatrix that should allow its users tospecify the value and line separators when inserting the matrix into anostream.

Two data members (e.g.,char const *d_valueSep andchar const*d_lineSep) are defined (and initialized to acceptable values). Theinsertion function insertsd_valueSep between values, andd_lineSep atthe end of inserted rows. The memberoperator()(char const *valueSep, charconst *lineSep) simply assigns values to the corresponding data members.

Given an objectMatrix matrix, then at this pointmatrix(" ", "\n")can be called. The function call operator should probably not insert thematrix, as the responsibility of manipulators is to manipulate, not toinsert. So, to insert a matrix a statement like

        cout << matrix(" ", "\n") << matrix << '\n';

should probably be used. The manipulator (i.e., function call operator)assigns the proper values tod_valueSep andd_lineSep, which are thenused during the actual insertion.

The return value of the function call operator remains to be specified. Thereturn value should be insertable, but in fact should not insert anything atall. An empty NTBS could be returned, but that's a bit kludge-like. Insteadthe address of a manipulator function, not performing any action, can bereturned. Here's the implementation of such an empty manipulator:

        // static       (alternatively a free function could be used)        std::ostream &Matrix::nop(std::ostream &out)        {            return out;        }

Thus, the implementation of theMatrix's manipulator becomes:

        std::ostream &(             *Matrix::operator()(char const *valueSep, char const *lineSep) )                                                             (std::ostream &)        {            d_valueSep = valueSep;            d_lineSep = lineSep;            return nop;        }

Instead (probably a matter of taste) of returning the address of an emptyfunction the manipulator could first set the required insertion-specificvalues and could then return itself: theMatrix would be insertedaccording to the just assigned values to the insertion variables:

        Matrix const &Matrix::operator()            (char const *valueSep, char const *lineSep)        {            d_valueSep = valueSep;            d_lineSep = lineSep;            return *this;        }

In this case the insertion statement is simplified to

    cout << matrix(" ", "\n") << '\n';

11.12: Lambda expressions

C++ supportslambda expressions. As we'll see inchapter19generic algorithms often accept arguments that caneither be function objects or plain functions. Examples are thesort(cf. section19.1.54) andfind_if (cf. section19.1.15) genericalgorithms. As arule of thumb: when a called function must remember itsstate a function object is appropriate, otherwise a plain function can beused.

Frequently the function or function object is not readily available, and itmust be defined in or near the location where it is used. This is commonlyrealized by defining a class or function in the anonymous namespace (say:class or function A), passing an A to the code needing A. If that code isitself a member function of the class B, then A's implementation might benefitfrom having access to the members of class B.

This scheme usually results in a significant amount of code (defining theclass), or it results in complex code (to make available software elementsthat aren't automatically accessible to A's code). It may also result in codethat is irrelevant at the current level of specification. Nested classes don'tsolve these problems either. Moreover, nested classes can't be used intemplates.

lambda expressions solve these problems. Alambda expression defines ananonymous function object which may immediately be passed to functionsexpecting function object arguments, as explained in the next few sections.

According to the C++ standard, lambda expressionsprovide a concise way tocreate simple function objects. The emphasis here is onsimple: a lambdaexpression's size should be comparable to the size of inline-functions: justone or maybe two statements. If you need more code, then encapsulate that codein a separate function which is then called from inside the lambdaexpression's compound statement, or consider designing a separate functionobject.

11.12.1: Lambda expressions: syntax

Alambda expression defines ananonymous function object, also called aclosure object or simply aclosure.

When a lambda expression is evaluated it results in a temporary functionobject (the closure object). This temporary function object is of a uniqueanonymous class type, called itsclosure type.

Lambda expressions are used inside blocks, classes or namespaces (i.e., prettymuch anywhere you like). Their implied closure type is defined in the smallestblock, class or namespace scope containing the lambda expression. The closureobject's visibility starts at its point of definition and ends where itsclosure type ends (i.e., their visibility is identical to the visibility ofplain variables).

The closure type defines aconst public inline function calloperator. Here is an example of a lambda expression:

    []                      // the `lambda-introducer'    (int x, int y)          // the `lambda-declarator'    {                       // a normal compound-statement        return x * y;    }

The function (formally: the function call operator of the closure typecreated by this lambda expression) expects twoint arguments and returnstheir product. This function is an inlineconst member of its closuretype. Itsconst attribute is removed if the lambda expression specifiesmutable. E.g.,

    [](int x, int y) mutable    ...

The lambda-declarator may be omitted if no parameters are defined, butwhen specifyingmutable (orconstexpr, see below) a lambda-declaratormust be specified (at least as an empty set of parentheses). The parameters ina lambda declarator cannot be given default arguments.

Declarator specifiers can bemutable,constexpr, or both. Aconstexpr lambda-expression is itself aconstexpr, which may becompile-time evaluated if its arguments qualify as const-expressions. Byimplication, if a lambda-expression is defined inside aconstexpr functionthen the lambda-expression itself is aconstexpr, and theconstexprdeclarator specifier is not required. Thus, the following function definitionsare identical:

    int constexpr change10(int n)    {        return [n]                {                    return n > 10 ? n - 10 : n + 10;                }();    }        int constexpr change10(int n)    {        return [n] () constexpr                {                    return n > 10 ? n - 10 : n + 10;                }();    }

A closure object as defined by the previous lambda expression could forexample be used in combination with theaccumulate generic algorithm(cf. section19.1.2) to compute the product of a series ofint valuesstored in a vector:

    cout << accumulate(vi.begin(), vi.end(), 1,                [] (int x, int y)                 {                     return x * y;                 }            );

This lambda expression implicitly defines its return type asdecltype(x * y). Implicit return types can be used in thesecases:

If there are multiplereturn statements returning values of differenttypes then the lambda expression's return type must explicitly be specifiedusing alate-specified return type,(cf. section3.3.7):

    [](bool neg, double y) -> int    {        return neg ? -y : y;    }

Variables visible at the location of a lambdaexpression may be accessible from inside the lambda expression's compoundstatement. Which variables and how they are accessed depends on the contentof the lambda-introducer.

When the lambda expression is defined inside a class member function thelambda-introducer may containthis or*this; where used in thefollowing overview this class-context is assumed.

Global variables are always accessible, and can be modified if theirdefinitions allow so (this in general holds true in the following overview:when stated that `variables can be modified' then that only applies tovariables that themselves allow modifications).

Local variables of the lambda expression's surrounding function may also be specified inside the lambda-introducer. The specificationlocal is used to refer to any comma-separated list of local variablesof the surrounding function that are visible at the lambda expression'spoint of definition. There is no required ordering of thethis, *this andlocal specifications.

Finally, where in the following overviewmutable is mentioned it must bespecified, wheremutable_opt is specified it is optional.

Access globals, maybe data members and local variables, define datamembers of lambda expressions:

The following specifications must use= as the first element of the lambda-introducer. It allows accessing local variables by value, unless...:

The following specifications must use& as the first element of the lambda-introducer. It allows accessing local variables by reference, unless...:

Even when not specified, lambda expressions implicitly capture theirthispointers, and class members are always accessed relative tothis. But whenmembers are called asynchronously (cf. chapter20) a problem mayarise, because the asynchronously called lambda function may refer to membersof an object whose lifetime ended shortly after asynchronously calling thelambda function. This potential problem is solved by using `*this' in thelambda-capture if it starts with=, e.g.,[=, *this] (in addition,variables may still also be captured, as usual). When specifying `*this'the object to whichthis refers isexplicitly captured: if theobject's scope ends it is not immediately destroyed, but its lifetime isextended by the lambda-expression for the duration of that expression. Inorder to use the `*this' specification, the object must beavailable. Consider the following example:

    struct S2     {        double ohseven = .007;        auto f()         {            return [this]                       // (1, see below)                   {                        return [*this]          // (2)                               {                                    return ohseven; // OK                               };                   }();                         // (3)        }        auto g()         {            return []                    {                        return [*this]                         {                             // error: *this not captured by                             // the outer lambda-expression                         };                     }();        }    };

Although lambda expressions are anonymous function objects, theycan beassigned to variables. Often, the variable is defined using the keywordauto. E.g.,

    auto sqr = [](int x)               {                   return x * x;               };

The lifetime of such lambda expressions is equal to the lifetime of thevariable receiving the lambda expression as its value.

Note also that defining a lambda expression is different from calling itsfunction operator. The functionS2::f() returns what the lamba expression(1)'s function call operator returns: its function call operator is called byusing() (at (3)). What it in fact returns is another anonymous functionobject (defined at(2)). As that's just a function object, to retrieve itsvalue it must still be called fromf's return value using something likethis:

    S2 s2;    s2.f()();

Here, the second set of parentheses activates the returned functionobject's function call operator. Had the parentheses been omitted at (3) thenS2::f() would have returned a mere anonymous function object (defined at(1)), in which case it would require three sets of parentheses to retrieveohseven's value:s2.f()()().

11.12.2: Using lambda expressions

Now that the syntax of lambda expressions have been covered let's see howthey can be used in various situations.

First we consider named lambda expressions. Named lambda expressions nicelyfit in the niche of local functions: when a function needs to perform computations which areat a conceptually lower level than the function's task itself, then it'sattractive to encapsulate these computations in a separate support functionand call the support function where needed. Although support functions can bedefined in anonymous namespaces, that quickly becomes awkward when therequiring function is a class member and the support function also must accessthe class's members.

In that case a named lambda expression can be used: it can be defined insidea requiring function, and it may be given full access to the surroundingclass. The name to which the lambda expression is assigned becomes the name ofa function which can be called from the surrounding function. Here is anexample, converting a numeric IP address to a dotted decimal string, which canalso be accessed directly from anDotted object (all implementationsin-class to conserve space):

    class Dotted    {        std::string d_dotted;                public:            std::string const &dotted() const            {                return d_dotted;            }            std::string const &dotted(size_t ip)            {                auto octet =                     [](size_t idx, size_t numeric)                    {                        return to_string(numeric >> idx * 8 & 0xff);                    };                d_dotted =                         octet(3, ip) + '.' + octet(2, ip) + '.' +                        octet(1, ip) + '.' + octet(0, ip);                return d_dotted;            }    };

Next we consider the use of generic algorithms, like thefor_each (cf. section19.1.18):

    void showSum(vector<int> const &vi)    {        int total = 0;        for_each(            vi.begin(), vi.end(),            [&](int x)            {                total += x;            }        );        std::cout << total << '\n';    }

Here the variableint total is passed to the lambda expression byreference and is directly accessed by the function. Its parameter list merelydefines anint x, which is initialized in sequence by each of the valuesstored invi. Once the generic algorithm has completedshowSum'svariabletotal has received a value that is equal to the sum of all thevector's values. It has outlived the lambda expression and its value isdisplayed.

But although generic algorithms are extremely useful, there may not always beone that fits the task at hand. Furthermore, an algorithm likefor_eachlooks a bit unwieldy, now that the language offers range-based for-loops. Solet's try this, instead of the above implementation:

    void showSum(vector<int> const &vi)    {        int total = 0;        for (auto el: vi)            [&](int x)            {                total += x;            };        std::cout << total << '\n';    }

But whenshowSum is now called, itscout statement consistentlyreports 0. What's happening here?

When a generic algorithm is given a lambda function, its implementationinstantiates a reference to a function. The referenced function is thereuponcalled from within the generic algorithm. But, in the above example therange-based for-loop's nested statement merely represents thedefinitionof a lambda function. Nothing is actually called, and hencetotal remainsequal to 0.

Thus, to make the above example work we not only mustdefine thelambda expression, but we must alsocall the lambda function. We can dothis by giving the lambda function aname, and then call thelambda function by its given name:

    void showSum(vector<int> const &vi)    {        int total = 0;        for (auto el: vi)        {            auto lambda = [&](int x)                            {                                total += x;                            };            lambda(el);        }        std::cout << total << '\n';    }

In fact, there is no need to give the lambda function a name: theautolambda definition represents the lambda function, which could alsodirectly be called. The syntax for doing this may look abit weird, but there's nothing wrong with it, and it allows us to drop thecompound statement, required in the last example, completely. Here goes:

    void showSum(vector<int> const &vi)    {        int total = 0;        for (auto el: vi)            [&](int x)            {                total += x;            }(el);          // immediately append the                             // argument list to the lambda                            // function's definition        std::cout << total << '\n';    }

Lambda expressions can also be used to prevent spurious returns fromcondition_variable's wait calls (cf. section20.4.3).

The classcondition_variable allows us to do so by offeringwaitmembers expecting a lockand a predicate. The predicate checks the data'sstate, and returnstrue if the data's state allows the data'sprocessing. Here is an alternative implementation of thedown member shownin section20.4.3, checking for the data's actual availability:

    void down()    {        unique_lock<mutex> lock(sem_mutex);        condition.wait(lock,             [&]()            {                return semaphore != 0            }        );        --semaphore;    }

The lambda expression ensures thatwait only returns oncesemaphore has been incremented.

Lambda expression are primarily used to obtain functors that are used in avery localized section of a program. Since they are used inside an existingfunction we should realize that once we use lambda functions multipleaggregation levels are mixed. Normally a function implements a task which canbe described at its own aggregation level using just a few sentences. E.g.,``the functionstd::sort sorts a data structure by comparing its elementsin a way that is appropriate to the context wheresort is called''. Byusing an existing comparison method the aggregation level is kept, and thestatement is clear by itself. E.g.,

    sort(data.begin(), data.end(), greater<DataType>());

If an existing comparison method is not available, a tailor-made functionobject must be created. This could be realized using a lambdaexpression. E.g.,

    sort(data.begin(), data.end(),         [&](DataType const &lhs, DataType const &rhs)        {            return lhs.greater(rhs);        }    );

Looking at the latter example, we should realize that here two differentaggregation levels are mixed: at the top level the intent is to sort theelements indata, but at the nested level (inside the lambda expression)something completely different happens. Inside the lambda expression we definehow a the decision is made about which of the two objects is the greater. Codeexhibiting such mixed aggregation levels is hard to read, and should beavoided.

On the other hand: lambda expressions also simplify code because the overheadof defining tailor-made functors is avoided. The advice, therefore, is to uselambda expressions sparingly.When they are used make sure that theirsizes remain small. As arule of thumb: lambda expressions should betreated like in-line functions, and should merely consist of one, or maybeoccasionally two expressions.

A special group of lambda expressions is known asgeneric lambdaexpressions. As generic lambda expressions are in factclass templates,their coverage is postponed until chapter22.

11.13: The case of [io]fstream::open()

Earlier, in section6.4.2.1, it was noted that the[io]fstream::open members expect anios::openmode value as theirfinal argument. E.g., to open anfstream object for writing you could doas follows:
    fstream out;    out.open("/tmp/out", ios::out);

Combinations are also possible. To open anfstream object forboth reading and writing the following stanza is often seen:

    fstream out;    out.open("/tmp/out", ios::in | ios::out);

When trying to combine enum values using a `home made'enum we may runinto problems. Consider the following:

    enum Permission    {        READ =      1 << 0,        WRITE =     1 << 1,        EXECUTE =   1 << 2    };    void setPermission(Permission permission);    int main()    {        setPermission(READ | WRITE);    }

When offering this little program to the compiler it replies with anerror message like this:

invalid conversion from 'int' to 'Permission'

The question is of course: why is it OK to combineios::openmodevalues passing these combined values to the stream'sopen member, butnot OK to combinePermission values.

Combining enum values using arithmetic operators results inint-typedvalues.Conceptually this never was our intention. Conceptually it can beconsidered correct to combine enum values if the resulting value conceptuallymakes sense as a value that is still within the original enumerationdomain. Note that after adding a valueREADWRITE = READ | WRITE to theaboveenum we're still not allowed to specifyREAD | WRITE as anargument tosetPermission.

To answer the question about combining enumeration values and yet staywithin the enumeration's domain we turn to operator overloading. Up to thispoint operator overloading has been applied to class types. Free functionslikeoperator<< have been overloaded, and those overloads are conceptuallywithin the domain of their class.

AsC++ is a strongly typed language realize that defining anenum isreally something beyond the mere association ofint-values with symbolicnames. An enumeration type is really a type of its own, and as with any typeits operators can be overloaded. When writingREAD | WRITE the compilerperforms the default conversion from enum values toint values andapplies the operator toints. It does so when it has no alternative.

But it is also possible to overload the enum type's operators. Thus we mayensure that we'll remain within the enum's domain even though the resultingvalue wasn't defined by the enum. The advantage of type-safety and conceptualclarity is considered to outweigh the somewhat peculiar introduction of valueshitherto not defined by the enum.

Here is an example of such an overloaded operator:

    Permission operator|(Permission left, Permission right)    {        return static_cast<Permission>(static_cast<int>(left) | right);    }

Other operators can easily and analogously be constructed.

Operators like the above were defined for theios::openmodeenumeration type, allowing us to specifyios::in | ios::out as argument toopen while specifying the corresponding parameter asios::openmodeas well. Clearly, operator overloading can be used in many situations, notnecessarily only involving class-types.

11.14: User-defined literals

In addition to the well-known literals, like numerical constants (with orwithout suffixes), character constants and string (textual) literals,C++also supportsuser-defined literals, also known asextensible literals.

A user-defined literal is defined by a function (see also section23.3)that must be defined at namespace scope. Such a function is called aliteral operator. A literal operator cannot be a class member function. The names of aliteral operator must start with an underscore, and a literal operator isused (called) bysuffixing its name (including the underscore) to theargument that must be passed to it . Assuming_NM2km (nautical mile to km)is the name of a literal operator, then it could be called as100_NM2km,producing, e.g., the value 185.2.

UsingType to represent the return type of the literal operatorits generic declaration looks like this:

    Type operator "" _identifier(parameter-list);

The blank space trailing the empty string is required. The parameter listsof literal operators can be:

If literal operators are overloaded the compiler will pick the literaloperator requiring the least `effort'. E.g., 120 is processed by a literaloperator defining anunsigned long long int parameter and not by itsoverloaded version, defining achar const * parameter. But if overloadedliteral operators exist definingchar const * andlong doubleparameters then the operator defining achar const * parameter is usedwhen the argument 120 is provided, while the operator defining alongdouble parameter is used with the argument 120.3.

A literator operator can define any return type. Here isan example of a definition of the_NM2km literal operator:

    double operator "" _NM2km(char const *nm)    {        return std::stod(nm) * 1.852;    }    double value = 120_NM2km;   // example of use

Of course, the argument could also have been along doubleconstant. Here's an alternative implementation, explicitly expecting alongdouble:

    double constexpr operator "" _NM2km(long double nm)    {        return nm * 1.852;    }    double value = 450.5_NM2km;   // example of use

A numeric constant can also be processed completely at compile-time.Section23.3 provides the details of this type of literal operator.

Arguments to literal operators are themselves always constants. A literaloperator like_NM2km cannot be used to convert, e.g., the value of avariable. A literal operator, although it is defined as a function, cannot becalled like a function. The following examples thereforeresult in compilation errors:

    double speed;    speed_NM2km;        // no identifier 'speed_NM2km'    _NM2km(speed);      // no function _NM2km    _NM2km(120.3);      // no function _NM2km

11.15: Overloadable operators

The following operators can be overloaded:
    +       -       *       /       %       ^       &       |    ~       !       ,       =       <=>     <       >       <=    >=      ++      --      <<      >>      ==      !=      &&    ||      +=      -=      *=      /=      %=      ^=      &=    |=      <<=     >>=     []      ()      ->      ->*     new    new[]   delete  delete[]

Several operators havetextual alternatives:


textual alternative operator

and &&
and_eq &=
bitand &
bitor |
compl ~
not !
not_eq !=
or ||
or_eq |=
xor ^
xor_eq ^=

`Textual' alternatives of operators are also overloadable (e.g.,operator and). However, note that textual alternatives are notadditional operators. So, within the same contextoperator&& andoperator and can notboth be overloaded.

Several of these operators may only be overloaded as member functionswithin a class. Thisholds true for the'=', the'[]', the'()' and the'->'operators. Consequently, it isn't possible to redefine, e.g., the assignmentoperator globally in such a way that it accepts achar const * as anlvalue and aString & as anrvalue. Fortunately, that isn'tnecessary either, as we have seen in section11.3.

Finally, the following operators cannot be overloaded:

    .       .*      ::      ?:      sizeof  typeid




[8]ページ先頭

©2009-2025 Movatter.jp