Главная страница
Навигация по странице:

  • 15.9.1. Конвертеры

  • 15.9.2. Конструктор как конвертер

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница71 из 93
    1   ...   67   68   69   70   71   72   73   74   ...   93

    742
    }
    Откомпилированная программа выдает следующие результаты:
    Введите SmallInt, пожалуйста: 127
    Прочитано значение 127
    Оно равно 127
    Введите SmallInt, пожалуйста (ctrl-d для выхода): 126
    Оно меньше, чем 127
    Введите SmallInt, пожалуйста (ctrl-d для выхода): 128
    Оно больше, чем 127
    Введите SmallInt, пожалуйста (ctrl-d для выхода): 256
    ***
    Ошибка диапазона SmallInt: 256 ***
    В реализацию класса SmallInt добавили поддержку новой функциональности:
    };
    #include
    #include "SmallInt.h" int main() { cout << "
    Введите SmallInt, пожалуйста: "; while ( cin >> si1 ) { cout << "
    Прочитано значение "
    << si1 << "\n
    Оно ";
    // SmallInt::operator int() вызывается дважды cout << ( ( si1 > 127 )
    ? "
    больше, чем "
    : ( ( si1 < 127 )
    ? "
    меньше, чем "
    : "
    равно ") ) << "127\n"; cout << "\
    Введите SmallInt, пожалуйста \
    (ctrl-d для выхода): ";
    } cout <<"
    До встречи\n";
    #include class SmallInt { friend istream& operator>>( istream &is, SmallInt &s ); friend ostream& operator<<( ostream &is, const SmallInt &s )
    { return os << s.value; } public:
    SmallInt( int i=0 ) : value( rangeCheck( i ) ){} int operator=( int i )
    { return( value = rangeCheck( i ) ); } operator int() { return value; } private: int rangeCheck( int ); int value;

    С++ для начинающих
    743
    Ниже приведены определения функций-членов, находящиеся вне тела класса:
    }
    15.9.1.
    Конвертеры
    Конвертер – это особый случай функции-члена класса, реализующий определенное пользователем преобразование объекта в некоторый другой тип. Конвертер объявляется в теле класса путем указания ключевого слова operator, за которым следует целевой тип преобразования.
    Имя, находящееся за ключевым словом, не обязательно должно быть именем одного из встроенных типов. В показанном ниже классе Token определено несколько конвертеров.
    В одном из них для задания имени типа используется typedef tName, а в другом – тип класса SmallInt.
    };
    Обратите внимание, что определения конвертеров в типы SmallInt и int одинаковы.
    Конвертер Token::operator int() возвращает значение члена val. Поскольку val имеет тип SmallInt, то неявно применяется SmallInt::operator int() для преобразования val в тип int. Сам Token::operator int() неявно употребляется компилятором для преобразования объекта типа Token в значение типа int. Например, istream& operator>>( istream &is, SmallInt &si ) { int ix; is >> ix; si = ix; // SmallInt::operator=(int) return is;
    } int SmallInt::rangeCheck( int i )
    {
    /* если установлен хотя бы один бит, кроме первых восьми,
    * то значение слишком велико; сообщить и сразу выйти */ if ( i &

    0377 ) { cerr << "\n***
    Ошибка диапазона SmallInt: "
    << i << " ***" << endl; exit( -1 );
    } return i;
    #include "SmallInt.h" typedef char *tName; class Token { public:
    Token( char *, int ); operator SmallInt() { return val; } operator tName() { return name; } operator int() { return val; }
    // другие открытые члены private:
    SmallInt val; char *name;

    С++ для начинающих
    744
    этот конвертер используется для неявного приведения фактических аргументов t1 и t2 типа Token к типу int формального параметра функции print():
    }
    После компиляции и запуска программа выведет такие строки: print( int ) : 255
    Общий вид конвертера следующий: operator type(); где type может быть встроенным типом, типом класса или именем typedef. Конвертеры, в которых type – тип массива или функции, не допускаются. Конвертер должен быть функцией-членом. В его объявлении не должны задаваться ни тип возвращаемого значения, ни список параметров:
    };
    Конвертер вызывается в результате явного преобразования типов. Если преобразуемое значение имеет тип класса, у которого есть конвертер, и в операции приведения указан тип этого конвертера, то он и вызывается:
    #include "Token.h" void print( int i )
    { cout << "print( int ) : " << i << endl;
    }
    Token t1( "integer constant", 127 );
    Token t2( "friend", 255 ); int main()
    { print( t1 ); // t1.operator int() print( t2 ); // t2.operator int() return 0; print( int ) : 127 operator int( SmallInt & ); // ошибка: не член class SmallInt { public: int operator int(); // ошибка: задан тип возвращаемого значения operator int( int = 0 ); // ошибка: задан список параметров
    // ...

    С++ для начинающих
    745
    char *tokName = static_cast< char * >( tok );
    У конвертера Token::operator tName() может быть нежелательный побочный эффект.
    Попытка прямого обращения к закрытому члену Token::name помечается компилятором как ошибка: char *tokName = tok.name; // ошибка: Token::name - закрытый член
    Однако наш конвертер, разрешая пользователям непосредственно изменять Token::name, делает как раз то, от чего мы хотели защититься. Скорее всего, это не годится. Вот, например, как могла бы произойти такая модификация:
    *tokname = 'P'; // но теперь в члене name находится Punction!
    Мы намереваемся разрешить доступ к преобразованному объекту класса Token только для чтения. Следовательно, конвертер должен возвращать тип const char*: const char *pn2 = tok; // правильно
    Другое решение – заменить в определении Token тип char* на тип string из стандартной библиотеки C++:
    #include "Token.h"
    Token tok( "function", 78 );
    // функциональная нотация: вызывается Token::operator SmallInt()
    SmallInt tokVal = SmallInt( tok );
    // static_cast: вызывается Token::operator tName()
    #include "Token.h"
    Token tok( "function", 78 ); char *tokName = tok; // правильно: неявное преобразование typedef const char *cchar; class Token { public: operator cchar() { return name; }
    // ...
    };
    // ошибка: преобразование char* в const char* не допускается char *pn = tok;

    С++ для начинающих
    746
    };
    Семантика конвертера Token::operator string() состоит в возврате копии значения (а не указателя на значение) строки, представляющей имя лексемы. Это предотвращает случайную модификацию закрытого члена name класса Token.
    Должен ли целевой тип точно соответствовать типу конвертера? Например, будет ли в следующем коде вызван конвертер int(), определенный в классе Token? calc( tok );
    Если целевой тип (в данном случае double) не точно соответствует типу конвертера (в нашем случае int), то конвертер все равно будет вызван при условии, что существует последовательность стандартных преобразований, приводящая к целевому типу из типа конвертера. (Эти последовательности описаны в разделе 9.3.) При обращении к функции calc()
    вызывается Token::operator int() для преобразования tok из типа Token в тип int. Затем для приведения результата от типа int к типу double применяется стандартное преобразование.
    Вслед за определенным пользователем преобразованием допускаются только стандартные. Если для достижения целевого типа необходимо еще одно пользовательское преобразование, то компилятор не применяет никаких преобразований. Предположим, что в классе Token не определен operator int(), тогда следующий вызов будет ошибочным: calc( tok );
    Если конвертер Token::operator int() не определен, то приведение tok к типу int потребовало бы вызова двух определенных пользователем конвертеров. Сначала фактический аргумент tok надо было бы преобразовать из типа Token в тип SmallInt с помощью конвертера class Token { public:
    Token( string, int ); operator SmallInt() { return val; } operator string() { return name; } operator int() { return val; }
    // другие открытые члены private:
    SmallInt val; string name; extern void calc( double );
    Token tok( "constant", 44 );
    //
    Вызывается ли оператор int()? Да
    // применяется стандартное преобразование int --> double extern void calc( int );
    Token tok( "pointer", 37 );
    // если Token::operator int() не определен,
    // то этот вызов приводит к ошибке компиляции

    С++ для начинающих
    747
    Token::operator SmallInt() а затем результат привести к типу int – тоже с помощью пользовательского конвертера
    Token::operator int()
    Вызов calc(tok) помечается компилятором как ошибка, так как не существует неявного преобразования из типа Token в тип int.
    Если логического соответствия между типом конвертера и типом класса нет, назначение конвертера может оказаться непонятным читателю программы:
    };
    Какое значение должен вернуть конвертер int() класса Date? Сколь бы основательными ни были причины для того или иного решения, читатель останется в недоумении относительно того, как пользоваться объектами класса Date, поскольку между ними и целыми числами нет явного логического соответствия. В таких случаях лучше вообще не определять конвертер.
    15.9.2.
    Конструктор как конвертер
    Набор конструкторов класса, принимающих единственный параметр, например,
    SmallInt(int)
    класса SmallInt, определяет множество неявных преобразований в значения типа SmallInt. Так, конструктор SmallInt(int) преобразует значения типа int в значения типа SmallInt. calc( i );
    При вызове calc(i) число i преобразуется в значение типа SmallInt с помощью конструктора SmallInt(int), вызванного компилятором для создания временного объекта нужного типа. Затем копия этого объекта передается в calc(), как если бы вызов функции был записан в форме: class Date { public:
    // попробуйте догадаться, какой именно член возвращается! operator int(); private: int month, day, year; extern void calc( SmallInt ); int i;
    // необходимо преобразовать i в значение типа SmallInt
    // это достигается применением SmallInt(int)

    С++ для начинающих
    748
    }
    Фигурные скобки в этом примере обозначают время жизни данного объекта: он уничтожается при выходе из функции.
    Типом параметра конструктора может быть тип некоторого класса:
    };
    В таком случае значение типа SmallInt можно использовать всюду, где допустимо значение типа Number:
    }
    Если конструктор используется для выполнения неявного преобразования, то должен ли тип его параметра точно соответствовать типу подлежащего преобразованию значения?
    Например, будет ли в следующем коде вызван SmallInt(int), определенный в классе
    SmallInt
    , для приведения dobj к типу SmallInt? calc( dobj );
    Если необходимо, к фактическому аргументу применяется последовательность стандартных преобразований до того, как вызвать конструктор, выполняющий определенное пользователем преобразование.
    При обращении к функции calc()
    употребляется стандартное преобразование dobj из типа double в тип int. Затем уже для приведения результата к типу SmallInt вызывается SmallInt(int).
    //
    Псевдокод на C++
    // создается временный объект типа SmallInt
    {
    SmallInt temp = SmallInt( i ); calc( temp ); class Number { public:
    // создание значения типа Number из значения типа SmallInt
    Number( const SmallInt & );
    // ... extern void func( Number );
    SmallInt si(87); int main()
    { // вызывается Number( const SmallInt & ) func( si );
    // ... extern void calc( SmallInt ); double dobj;
    // вызывается ли SmallInt(int)? Да
    // dobj преобразуется приводится от double к int
    // стандартным преобразованием

    С++ для начинающих
    749
    Компилятор неявно использует конструктор с единственным параметром для преобразования его типа в тип класса, к которому принадлежит конструктор. Однако иногда удобнее, чтобы конструктор Number(const SmallInt&) можно было вызывать только для инициализации объекта типа Number значением типа SmallInt, но ни в коем случае не для выполнения неявных преобразований. Чтобы избежать такого употребления конструктора, объявим его явным (explicit):
    };
    Компилятор никогда не применяет явные конструкторы для выполнения неявных преобразований типов:
    }
    Однако такой конструктор все же можно использовать для преобразования типов, если оно запрошено явно в форме оператора приведения типа:
    }
    15.10.
    Выбор преобразования A
    Определенное пользователем преобразование реализуется в виде конвертера или конструктора. Как уже было сказано, после преобразования, выполненного конвертером, разрешается использовать стандартное преобразование для приведения возвращенного значения к целевому типу. Трансформации, выполненной конструктором, также может предшествовать стандартное преобразование для приведения типа аргумента к типу формального параметра конструктора. class Number { public:
    // никогда не использовать для неявных преобразований explicit Number( const SmallInt & );
    // ... extern void func( Number );
    SmallInt si(87); int main()
    { // ошибка: не существует неявного преобразования из SmallInt в Number func( si );
    // ...
    SmallInt si(87); int main()
    { // ошибка: не существует неявного преобразования из SmallInt в Number func( si ); func( Number( si ) ); // правильно: приведение типа func( static_cast< Number >( si ) ); // правильно: приведение типа

    С++ для начинающих
    750
    Последовательность определенных пользователем преобразований – это комбинация определенного пользователем и стандартного преобразования, которая необходима для приведения значения к целевому типу. Такая последовательность имеет вид:
    Последовательность стандартных преобразований ->
    Определенное пользователем преобразование ->
    Последовательность стандартных преобразований где определенное пользователем преобразование реализуется конвертером либо конструктором.
    Не исключено, что для трансформации исходного значения в целевой тип существует две разных последовательности пользовательских преобразований, и тогда компилятор должен выбрать из них лучшую. Рассмотрим, как это делается.
    В классе разрешается определять много конвертеров. Например, в нашем классе Number их два: operator int() и operator float(), причем оба способны преобразовать объект типа Number в значение типа float. Естественно, можно воспользоваться конвертером Token::operator float() для прямой трансформации. Но и
    Token::operator int()
    тоже подходит, так как результат его применения имеет тип int и, следовательно, может быть преобразован в тип float с помощью стандартного преобразования. Является ли трансформация неоднозначной, если имеется несколько таких последовательностей? Или какую-то из них можно предпочесть остальным? float ff = num; // какой конвертер? operator float()
    В таких случаях выбор наилучшей последовательности определенных пользователем преобразований основан на анализе последовательности преобразований, которая применяется после конвертера. В предыдущем примере можно применить такие две последовательности:
    1. operator float() -> точное соответствие
    2. operator int() -> стандартное преобразование
    Как было сказано в разделе 9.3, точное соответствие лучше стандартного преобразования.
    Поэтому первая последовательность лучше второй, а значит, выбирается конвертер
    Token::operator float()
    Может случиться так, что для преобразования значения в целевой тип применимы два разных конструктора. В этом случае анализируется последовательность стандартных преобразований, предшествующая вызову конструктора: class Number { public: operator float(); operator int();
    // ...
    };
    Number num;

    С++ для начинающих
    751
    }
    Здесь в классе SmallInt определено два конструктора – SmallInt(int) и
    SmallInt(double)
    , которые можно использовать для изменения значения типа double в объект типа SmallInt: SmallInt(double) трансформирует double в SmallInt напрямую, а SmallInt(int) работает с результатом стандартного преобразования double в int. Таким образом, имеются две последовательности определенных пользователем преобразований:
    1. точное соответствие -> SmallInt( double )
    2. стандартное преобразование -> SmallInt( int )
    Поскольку точное соответствие лучше стандартного преобразования, то выбирается конструктор SmallInt(double).
    Не всегда удается решить, какая последовательность лучше. Может случиться, что все они одинаково хороши, и тогда мы говорим, что преобразование неоднозначно. В таком случае компилятор не применяет никаких неявных трансформаций. Например, если в классе Number есть два конвертера:
    }; то невозможно неявно преобразовать объект типа Number в тип long. Следующая инструкция вызывает ошибку компиляции, так как выбор последовательности определенных пользователем преобразований неоднозначен: long lval = num;
    Для трансформации num в значение типа long применимы две такие последовательности:
    1. operator float() -> стандартное преобразование
    2. operator int() -> стандартное преобразование class SmallInt { public:
    SmallInt( int ival ) : value( ival ) { }
    SmallInt( double dval )
    : value( static_cast< int >( dval ) );
    { }
    }; extern void manip( const SmallInt & ); int main() { double dobj; manip( dobj ); // правильно: SmallInt( double ) class Number { public: operator float(); operator int();
    // ...
    // ошибка: можно применить как float(), так и int()

    С++ для начинающих
    752
    Поскольку в обоих случаях за использованием конвертера следует применение стандартного преобразования, то обе последовательности одинаково хороши и компилятор не может выбрать ни одну из них.
    С помощью явного приведения типов программист способен задать нужное изменение: long lval = static_cast< int >( num );
    Вследствие такого указания выбирается конвертер Token::operator int(), за которым следует стандартное преобразование в long.
    Неоднозначность при выборе последовательности трансформаций может возникнуть и тогда, когда два класса определяют преобразования друг в друга. Например: compute( num ); // ошибка: возможно два преобразования
    Аргумент num преобразуется в тип SmallInt двумя разными способами: с помощью конструктора SmallInt::SmallInt(const Number&) либо с помощью конвертера
    Number::operator SmallInt()
    . Поскольку оба изменения одинаково хороши, вызов считается ошибкой.
    Для разрешения неоднозначности программист может явно вызвать конвертер класса
    Number
    : compute( num.operator SmallInt() );
    Однако для разрешения неоднозначности не следует использовать явное приведение типов, поскольку при отборе преобразований, подходящих для приведения типов, рассматриваются как конвертер, так и конструктор: compute( SmallInt( num ) ); // ошибка: по-прежнему неоднозначно
    Как видите, наличие большого числа подобных конвертеров и конструкторов небезопасно, поэтому их. следует применять с осторожностью. Ограничить
    // правильно: явное приведение типа class SmallInt { public:
    SmallInt( const Number & );
    // ...
    }; class Number { public: operator SmallInt();
    // ...
    }; extern void compute( SmallInt ); extern Number num;
    // правильно: явный вызов устраняет неоднозначность

    С++ для начинающих
    1   ...   67   68   69   70   71   72   73   74   ...   93


    написать администратору сайта