Конструктор преобразования базового типа к типу определяемому разрабатываемым классом

Обновлено: 07.12.2022

В уроке «8.5 – Явное преобразование (приведение) типов данных и static_cast » вы узнали, что C++ позволяет преобразовывать один тип данных в другой. В следующем примере показано преобразование int в double :

C++ уже умеет преобразовывать встроенные типы данных. Однако он не знает, как преобразовывать какой-либо из наших пользовательских классов. Вот здесь и вступает в игру перегрузка операторов приведения типов.

Пользовательские преобразования позволяют нам преобразовать наш класс в другой тип данных. Взгляните на следующий класс:

Этот класс довольно прост: он содержит какое-то количество центов в виде числа int и предоставляет функции доступа для получения и установки количества центов. Также он предоставляет конструктор для преобразования int в Cents .

Если мы можем преобразовать int в Cents , то, возможно, будет полезно иметь возможность преобразовывать Cents обратно в int ? В некоторых случаях это может быть не так, но в данном случае это имеет смысл.

В следующем примере мы должны использовать getCents() для преобразования нашей переменной Cents обратно в число int , чтобы мы могли распечатать ее с помощью printInt() :

Если мы уже написали много функций, которые принимают числа int в качестве параметров, наш код будет завален вызовами getCents() , что сделает его более беспорядочным, чем нужно.

Чтобы упростить задачу, мы можем предоставить пользовательское преобразование, перегрузив приведение типа в int . Это позволит нам напрямую преобразовать наш класс Cents в тип int . В следующем примере показано, как это делается:

Следует отметить три вещи:

  1. Чтобы перегрузить функцию, которая приводит наш класс к типу int , мы пишем в нашем классе новую функцию с именем operator int() . Обратите внимание, что между словом operator и типом, к которому мы выполняем преобразование, есть пробел.
  2. Пользовательские преобразования не принимают параметров, так как нет возможности передать им аргументы.
  3. Пользовательские преобразования не имеют возвращаемого типа. C++ предполагает, что вы вернете правильный тип.

Теперь в нашем примере мы можем вызвать printInt() следующим образом:

Компилятор сначала заметит, что функция printInt принимает параметр типа int . Затем он заметит, что переменная cents не является int . Наконец, он станет проверять, предоставили ли мы способ преобразования Cents в int . Поскольку этот способ у нас есть, он вызовет нашу функцию operator int() , которая возвращает int , и возвращенный int будет передан в printInt() .

Теперь мы также можем явно преобразовать нашу переменную Cents в int :

Вы можете предоставить пользовательские преобразования в любой тип данных, который захотите, включая ваши собственные пользовательские типы данных!

Вот новый класс под названием Dollars , который обеспечивает перегруженное преобразование в Cents :

Это позволяет нам преобразовывать объект Dollars напрямую в объект Cents ! Это позволяет делать что-то вроде этого:

Преобразование создает новое значение некоторого типа из значения другого типа. Стандартные преобразования встроены в язык C++ и поддерживают встроенные типы, и вы можете создавать пользовательские преобразования для выполнения преобразований в определяемые пользователем типы или между ними.

Стандартные преобразования выполняют преобразование между встроенными типами, между указателями или ссылками на типы, связанные наследованием, в и из указателей void и в пустой указатель. Дополнительные сведения см. в разделе "Стандартные преобразования". Пользовательские преобразования выполняют преобразование между пользовательскими типами или между пользовательскими и встроенными типами. Их можно реализовать как конструкторы преобразования или как функции преобразования.

Преобразования могут быть явными, когда программист вызывает преобразование одного типа в другой (как в приведении или прямой инициализации) или неявными, когда язык или программа вызывают типы, которые отличаются от заданных программистом.

Попытка неявного преобразования выполняется, когда

тип аргумента, предоставленного для функции, не совпадает с соответствующим параметром;

тип значения, возвращаемого функцией, не совпадает с типом возвращаемого значения функции;

тип выражения инициализатора не совпадает с типом инициализируемого объекта;

тип результата выражения, которое управляет условным оператором, циклической конструкцией или параметром, не совпадает с тем, который требуется для управления;

тип операнда, предоставленного для оператора, не совпадает с соответствующим параметром операнда. Для встроенных операторов тип обоих операндов должен совпадать; он преобразуется в общий тип, который может представлять оба операнда. Дополнительные сведения см. в разделе "Стандартные преобразования". Для пользовательских операторов тип каждого операнда должен совпадать с соответствующим параметром операнда.

Если не удается выполнить неявное преобразование с помощью стандартного преобразования, компилятор может использовать пользовательское преобразование, за которым (при необходимости) будет следовать дополнительное стандартное преобразование.

Если на сайте преобразования есть два и более пользовательских преобразования, выполняющих одно преобразование, преобразование называется неоднозначным. Неоднозначность подразумевает ошибку, так как компилятор не может определить, какое из доступных преобразований выбрать. Тем не менее, не будет ошибкой определить несколько способов выполнения одного преобразования, так как набор доступных преобразований может отличаться в разных участках исходного кода, например в зависимости от того, какие файлы заголовков входят в исходный файл. Пока на сайте преобразования доступно только одно преобразование, о неоднозначности речь не идет. Существует несколько путей возникновения неоднозначных преобразований, однако самые распространенные перечислены ниже.

Множественное наследование. Преобразование определено в нескольких базовых классах.

Вызов неоднозначной функции. Преобразование определено как конструктор преобразования типа целевого объекта и как функция преобразования типа источника. Дополнительные сведения см. в разделе "Функции преобразования".

Неоднозначность, как правило, можно устранить, просто более полно указав имя соответствующего типа или выполнив явное приведение для пояснения намерения.

Конструкторы преобразования и функции преобразования подчиняются правилам управления доступом членов, однако доступность преобразований учитывается, только если можно определить неоднозначное преобразование. Это означает, что преобразование может быть неоднозначным, даже если уровень доступа конкурирующего преобразования будет блокировать его использование. Дополнительные сведения о специальных возможностях членов см. в разделе "Контроль доступа членов".

Ключевое слово explicit и проблемы с неявным преобразованием

По умолчанию при создании пользовательского преобразования компилятор может использовать его для выполнения неявных преобразований. Иногда это совпадает с вашими намерениями, но в других случаях простые правила, которые определяют выполнение неявных преобразований компилятором, могут привести к тому, что он примет нежелательный код.

Одним из известных примеров неявного преобразования, которое может вызвать проблемы, является преобразование в bool . Существует множество причин, по которым может потребоваться создать тип класса, который можно использовать в логическом контексте( например, для управления оператором if или циклом), но когда компилятор выполняет пользовательское преобразование во встроенный тип, компилятор может применить дополнительное стандартное преобразование. Цель этого дополнительного стандартного преобразования заключается в том, чтобы разрешить такие вещи, как повышение до short int , но он также открывает дверь для менее очевидных преобразований, например от bool к int , что позволяет использовать тип класса в целочисленных контекстах, которые вы никогда не намеревались. Эта конкретная проблема известна как Сейф bool Problem. Эта проблема заключается в том, что ключевое explicit слово может помочь.

Ключевое explicit слово сообщает компилятору, что указанное преобразование нельзя использовать для выполнения неявных преобразований. Если требуется синтаксическое удобство неявных преобразований перед вводом ключевого explicit слова, необходимо либо принять непредвиденные последствия, которые иногда создаются неявным преобразованием или используются менее удобные именованные функции преобразования в качестве обходного решения. Теперь, используя ключевое explicit слово, можно создавать удобные преобразования, которые можно использовать только для выполнения явных приведения или прямой инициализации, и это не приведет к таким проблемам, как Сейф Bool Problem.

Ключевое explicit слово можно применить к конструкторам преобразования с C++98, а также к функциям преобразования с C++11. В следующих разделах содержатся дополнительные сведения об использовании ключевого explicit слова.

Конструкторы преобразования

Конструкторы преобразования определяют преобразование из пользовательских или встроенных типов в пользовательские типы. В следующем примере демонстрируется конструктор преобразования, который преобразует встроенный тип double в определяемый пользователем тип Money .

Обратите внимание, что первый вызов функции display_balance , которая принимает аргументы типа Money , не требует преобразования, так как аргумент принадлежит к правильному типу. Однако при втором вызове display_balance требуется преобразование, так как тип аргумента со значением 49.95 , а не то, double что ожидает функция. Функция не может использовать это значение напрямую, но так как имеется преобразование из типа аргумента ( double в тип соответствующего параметра) Money — временное значение типа Money создается из аргумента и используется для завершения вызова функции. В третьем вызове display_balance обратите внимание, что аргумент не является аргументом double , а имеет float значение 9.99 ,и все же вызов функции может быть завершен, так как компилятор может выполнить стандартное преобразование ( в данном случае — от double float ) и затем выполнить определяемое пользователем преобразование double для Money завершения необходимого преобразования.

Объявление конструкторов преобразования

Следующие правила применяются к объявлению конструктора преобразования.

Целевым типом преобразования является сконструированный пользовательский тип.

Конструкторы преобразований, как правило, принимают только один аргумент типа источника. Однако конструктор преобразования может указывать дополнительные параметры, если у каждого из них есть значение по умолчанию. Тип источника остается типом первого параметра.

Конструкторы преобразований, как и все конструкторы, не указывают тип возвращаемого значения. Указание типа возвращаемого значения в объявлении является ошибкой.

Конструкторы преобразования могут быть явными.

Явные конструкторы преобразования

Объявляя конструктор explicit преобразования, его можно использовать только для выполнения прямой инициализации объекта или выполнения явного приведения. Это не дает функциям, которые принимают аргумент типа класса, также неявно принимать аргументы типа источника конструктора преобразования, а также блокирует инициализацию копирования типа класса из значения типа источника. В следующем примере демонстрируется определение явного конструктора преобразования и влияние на правильный синтаксис кода.

В этом примере обратите внимание, что явный конструктор преобразования можно использовать для выполнения прямой инициализации типа payable . Если же вы попытаетесь выполнить инициализацию копирования Money payable = 79.99; , это приведет к ошибке. Первый вызов display_balance не включает преобразование, так как указан аргумент правильного типа. Второй вызов display_balance является ошибкой, так как конструктор преобразования нельзя использовать для выполнения неявного преобразования. Третий вызов является законным из-за явного приведения Money , но обратите внимание, что компилятор по-прежнему display_balance помог завершить приведение путем вставки неявного приведения из float . double

Несмотря на то, что использование неявных преобразований кажется удобным, в результате могут возникать трудновыявляемые ошибки. Как показывает опыт, лучше всего объявлять все конструкторы преобразований явными за исключением тех случаев, когда необходимо, чтобы определенное преобразование выполнялось неявно.

Функции преобразования

Функции преобразования определяют преобразования из пользовательского в другие типы. Эти функции иногда называют "операторами приведения", так как они, наряду с конструкторами преобразования, вызываются, когда значение приводится к другому типу. В следующем примере демонстрируется функция преобразования, которая преобразуется из определяемого пользователем типа в Money встроенный тип double :

Обратите внимание, что переменная-член amount является закрытой и что общедоступная функция преобразования в тип double вводится только для возврата значения amount . В функции display_balance неявное преобразование возникает, когда значение balance направляется в стандартный вывод с помощью оператора вставки в поток double компилятор может использовать функцию Money double преобразования для удовлетворения оператора вставки потока.

Функции преобразования наследуются производными классами. Функции преобразования в производном классе переопределяют наследуемую функцию преобразования, только когда выполняют преобразование в точно такой же тип. Например, определяемая пользователем функция преобразования производного оператора класса int не переопределяет (или даже не влияет) определяемую пользователем функцию преобразования оператора базового класса short, даже если стандартные преобразования определяют связь преобразования между int и short .

Объявление функций преобразования

Следующие правила применяются к объявлению функции преобразования.

Целевой тип преобразования должен быть объявлен до объявления функции преобразования. Классы, структуры, перечисления и определения типа нельзя объявлять в объявлении функции преобразования.

Функции преобразования не принимают аргументов. Указание любых параметров в объявлении является ошибкой.

Функции преобразования имеют тип возвращаемого значения, задаваемый именем функции преобразования, которое также является именем типа целевого объекта преобразования. Указание типа возвращаемого значения в объявлении является ошибкой.

Функции преобразования могут быть виртуальными.

Функции преобразования могут быть явными.

Явные функции преобразования

Если функция преобразования объявлена как явная, ее можно использовать только для выполнения явного приведения. Это не дает функциям, которые принимают аргумент типа целевого объекта функции преобразования, также неявно принимать аргументы типа класса, а также блокирует инициализацию копирования экземпляров типа целевого объекта из значения типа класса. В следующем примере демонстрируется определение явной функции преобразования и влияние на правильный синтаксис кода.

Здесь был сделан двойный оператор функции преобразования, и явное приведение к типу double было введено в функцию display_balance для выполнения преобразования. Если пропустить это преобразование, компилятор не сможет найти подходящий оператор вставки в поток

В этом разделе обсуждаются пользовательские преобразования (UDC), если один из типов в преобразовании является ссылкой или экземпляром типа значения или ссылочного типа.

Явные и неявные преобразования

Определяемое пользователем преобразование может быть либо неявным, либо явным. Если преобразование не приводит к утрате информации, то значение UDC должно быть неявным. В противном случае должна быть определена явная UDC.

Конструктор собственного класса можно использовать для преобразования ссылки или типа значения в собственный класс.

Дополнительные сведения о преобразованиях см. в разделе Преобразование упаковки и преобразования Standard.

Выходные данные

Операторы "преобразования из"

Операторы Convert-from создают объект класса, в котором оператор определен из объекта какого-либо другого класса.

Стандартный C++ не поддерживает операторы Convert-from; для этой цели в стандартном C++ используются конструкторы. Однако при использовании типов CLR Visual C++ предоставить синтаксическую поддержку для вызова операторов Convert-from.

Чтобы обеспечить взаимодействие с другими CLS-совместимыми языками, может потребоваться заключить каждый определенный пользователем унарный конструктор для заданного класса с помощью соответствующего оператора Convert-from.

Операторы Convert и from:

Должны быть определены как статические функции.

Может быть либо неявным (для преобразований, которые не теряют точность, например Short-to-int), либо явным, если возможна потеря точности.

Должен возвращать объект содержащего класса.

Должен иметь тип "from" как единственный тип параметра.

В следующем образце показан неявный и явный оператор преобразования, определяемый пользователем (UDC).

Выходные данные

Операторы Convert-to

Операторы Convert-to преобразуют объект класса, в котором оператор определен другим объектом. В следующем примере показан неявный оператор преобразования с преобразованием в, определяемый пользователем:

Выходные данные

Явный определяемый пользователем оператор преобразования-преобразования подходит для преобразований, которые могут привести к потере данных каким-либо образом. Чтобы вызвать явный оператор Convert-to, необходимо использовать приведение.

Выходные данные

Преобразование универсальных классов

Универсальный класс можно преобразовать в T.

Выходные данные

Конструктор преобразования принимает тип и использует его для создания объекта. Конструктор преобразования вызывается только с прямой инициализацией; приведения не будут вызывать конструкторы преобразования. По умолчанию для типов CLR конструкторы преобразования являются явными.

Выходные данные

В этом примере кода неявная функция статического преобразования делает то же самое, что и явный конструктор преобразования.

В предыдущем уроке «8.1 – Неявное преобразование (принуждение) типов данных» мы обсуждали, что компилятор может неявно преобразовывать значение из одного типа данных в другой с помощью системы, называемой неявным преобразованием типов. Если вы хотите выполнить числовое продвижение значения одного типа данных до более крупного аналогичного типа данных, то неявное преобразование типа использовать можно.

Однако многие начинающие программисты пробуют что-то вроде этого:

Поскольку 10 и 4 принадлежат целочисленному типу int , целочисленного продвижения не происходит. Выполняется целочисленное деление 10/4, в результате получается значение 2, которое затем неявно преобразуется в 2.0 и присваивается переменной d ! Скорее всего, это не то, что было задумано.

В случае, когда вы используете литеральные значения (например, 10 или 4), замена одного или обоих целочисленных литеральных значений на литеральное значение с плавающей запятой (10.0 или 4.0) приведет к преобразованию обоих операндов в значения с плавающей запятой, и деление будет выполнено с использованием математики с плавающей запятой (и, таким образом, сохранится дробная часть).

Но что, если вы используете переменные? Рассмотрим этот случай:

Переменная d получит значение 2.0. Как сообщить компилятору, что мы хотим использовать деление с плавающей запятой вместо целочисленного деления? Суффиксы литералов не могут использоваться с переменными. Нам нужен способ преобразовать переменные одного (или обоих) операндов в тип с плавающей запятой, чтобы использовалось деление с плавающей запятой.

К счастью, C++ поставляется с рядом различных операторов приведения типов (чаще называемых приведениями или англоязычный термин «cast»), которые могут использоваться программистом для запроса компилятора на выполнение преобразования типа. Поскольку приведение типов является явным запросом программиста, эту форму преобразования типа часто называют явным преобразованием типа (в отличие от неявного преобразования типа, когда компилятор выполняет преобразование типа автоматически).

Приведение типа

В C++ существует 5 различных видов приведений типа: приведения в стиле C, статические приведения, константные приведения, динамические приведения и реинтерпретирующие приведения. Последние четыре иногда называют именованными приведениями.

В этом уроке мы рассмотрим приведение типа в стиле C и статическое приведение.

Связанный контент

Динамические приведения мы рассмотрим в уроке «18.10 – Динамическое преобразование типов» после того, как рассмотрим другие необходимые темы.

Константных и реинтерпретирующих приведений обычно следует избегать потому, что они полезны только в редких случаях и могут быть вредны при неправильном использовании.

Предупреждение

Избегайте константных приведений и реинтерпретирующих приведений, если у вас нет веской причины их использовать.

Приведение типа в стиле C

В стандартном программировании на C приведение типов выполняется с помощью оператора () , при этом имя типа, в который необходимо преобразовать значение, помещается в круглые скобки. Вы всё еще можете увидеть, что они используются в коде, преобразованном из C.

В приведенном выше коде мы используем приведение в стиле C для типа с плавающей запятой, чтобы указать компилятору, преобразовать x в double . Поскольку левый операнд у operator/ теперь вычисляется как значение с плавающей запятой, правый оператор также будет преобразован в значение с плавающей запятой, и деление будет выполняться с использованием деления с плавающей запятой вместо целочисленного деления!

C++ также позволяет вам использовать приведение в стиле C с синтаксисом, более похожим на вызов функций:

Это работает идентично предыдущему примеру, но тут преимущество в том, что преобразуемое значение заключено в скобки (что упрощает определение того, что конвертируется).

Хотя приведение в стиле C выглядит как единое преобразование, на самом деле оно может выполнять множество различных преобразований в зависимости от контекста. Оно может включать в себя статическое приведение, константное приведение или реинтерпретирующее приведение (последних двух из которых, как мы упоминали выше, следует избегать). В результате приведение типов в стиле C подвержено риску непреднамеренного неправильного использования и не приводит к ожидаемому поведению, чего легко избежать, если вместо этого использовать приведение типов согласно C++.

Связанный контент.

Если вам интересно, в статье «За кулисами C++: статическое, реинтерпретирующее приведения типов и приведение типов в стиле C» есть дополнительная информация о том, как на самом деле работают приведения в стиле C.

Лучшая практика

Избегайте использования приведений в стиле C.

static_cast

В C++ появился оператор приведения типов static_cast , который можно использовать для преобразования значения одного типа в значение другого типа.

Ранее вы видели, как static_cast используется для преобразования char в int , чтобы std::cout печатал его как целое число, а не как символ:

Оператор static_cast принимает в качестве входного одно значение и выводит то же значение, преобразованное в тип, указанный в угловых скобках. static_cast лучше всего использовать для преобразования одного базового типа в другой.

Основное преимущество static_cast заключается в том, что он обеспечивает проверку типа во время компиляции, что затрудняет случайную ошибку. static_cast также (намеренно) менее мощный, чем приведение типов в стиле C, поэтому вы не можете случайно удалить const или выполнить другие вещи, которые вы, возможно, не собирались делать.

Лучшая практика

Используйте static_cast , когда вам нужно преобразовать значение из одного типа в другой.

Использование приведения типов, чтобы сделать неявное преобразование типов явным

Компиляторы часто жалуются на небезопасное (сужающее) неявное преобразование типа. Например, рассмотрим следующую программу:

Преобразование int (4 байта) в char (1 байт) потенциально небезопасно (поскольку компилятор не может определить, будет ли целое число выходить за пределы диапазона char или нет), поэтому компилятор обычно выводит предупреждение. Если бы мы использовали инициализацию списком, компилятор выдал бы ошибку.

Чтобы обойти это, мы можем использовать статическое приведение для явного преобразования нашего числа int в char :

Когда мы делаем так, мы явно сообщаем компилятору, что это преобразование преднамеренное, и принимаем на себя ответственность за последствия (например, за превышение диапазона char , если оно произойдет). Поскольку выходное значение этого static_cast имеет тип char , присвоение переменной ch не генерирует несоответствия типов и, следовательно, никаких предупреждений или ошибок.

В следующей программе компилятор обычно будет жаловаться на то, что преобразование double в int может привести к потере данных:

Чтобы сообщить компилятору, что мы преднамеренно хотим это сделать, можно написать так:

Небольшой тест

Вопрос 1

В чем разница между неявным и явным преобразованием типов?

Неявное преобразование типа выполняется всякий раз, когда ожидается один тип данных, но предоставляется другой тип данных.

Явное преобразование типа происходит, когда пользователь использует приведение типа для явного преобразования значения из одного типа в другой тип.

По умолчанию C++ обрабатывает любой конструктор как оператор неявного преобразования. Рассмотрим следующий случай:

Хотя функция printFraction() ожидает объект Fraction , вместо этого мы передали ей целочисленный литерал 6. Поскольку у Fraction есть конструктор, который принимает одно число int , компилятор неявно преобразует литерал 6 в объект Fraction . Для этого он инициализирует параметр f функции printFraction() с помощью конструктора Fraction(int, int) .

Следовательно, показанная выше программа печатает:

Это неявное преобразование работает для всех видов инициализации (прямой, унифицированной и копирующей).

Конструкторы, которые могут использоваться для неявных преобразований, называются конструкторами преобразования (или преобразующими конструкторами). До C++11 только конструкторы, принимающие один параметр, могли быть конструкторами преобразования. Однако с новым синтаксисом унифицированной инициализации в C++11 это ограничение было снято, и конструкторы, принимающие несколько параметров, теперь также могут быть конструкторами преобразования.

Ключевое слово explicit

Хотя выполнение неявных преобразований имеет смысл в случае с Fraction , в других случаях это может быть нежелательно или привести к неожиданному поведению:

В приведенном выше примере пользователь пытается инициализировать строку с помощью char . Поскольку char является частью семейства целочисленных типов, компилятор будет использовать конструктор преобразования MyString(int) , чтобы неявно преобразовать char в MyString . Затем программа напечатает этот объект MyString , что приведет к неожиданным результатам. Точно так же вызов printString('x') вызывает неявное преобразование, которое приводит к той же проблеме.

Один из способов решения этой проблемы – сделать конструкторы (и функции преобразования) явными с помощью ключевого слова explicit , которое помещается перед именем функции. Явные конструкторы и функции преобразования не будут использоваться для неявных преобразований или копирующей инициализации:

Приведенная выше программа не будет компилироваться, так как MyString(int) был сделан явным, и не удалось найти соответствующий конструктор преобразования для неявного преобразования ' x ' в MyString .

Однако обратите внимание, что создание явного конструктора предотвращает только неявные преобразования. Явные преобразования (через приведение типа) по-прежнему разрешены:

Прямая и унифицированная инициализации также по-прежнему преобразуют параметры для соответствия (унифицированная инициализация не приведет к сужающим преобразованиям, но с радостью выполнит другие типы преобразований).

Правило

Подумайте о том, чтобы сделать ваши конструкторы и пользовательские функции-члены преобразования явными, чтобы предотвратить ошибки неявного преобразования.

Ключевое слово delete

В нашем случае с MyString мы на самом деле хотим полностью запретить преобразование ' x ' в MyString (явное или неявное, поскольку результаты не будут интуитивно понятными). Один из способов частично сделать это – добавить конструктор MyString(char) и сделать его закрытым:

Однако этот конструктор по-прежнему можно использовать изнутри класса (закрытый доступ мешает только нечленам класса вызывать эту функцию).

Лучший способ решить проблему – использовать ключевое слово delete (введенное в C++11) для удаления функции:

Когда функция была удалена, любое использование этой функции считается ошибкой компиляции.

Обратите внимание, что конструктор копирования и перегруженные операторы также могут быть удалены, чтобы предотвратить их использование.

Читайте также: