Поделиться через


Заданные пользователем преобразования типов (C++)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#include <iostream>

class Money
{
public:
    Money() : amount{ 0.0 } {};
    Money(double _amount) : amount{ _amount } {};

    double amount;
};

void display_balance(const Money balance)
{
    std::cout << "The balance is: " << balance.amount << std::endl;
}

int main(int argc, char* argv[])
{
    Money payable{ 79.99 };

    display_balance(payable);
    display_balance(49.95);
    display_balance(9.99f);

    return 0;
}

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

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

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

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

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

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

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

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

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

#include <iostream>

class Money
{
public:
    Money() : amount{ 0.0 } {};
    explicit Money(double _amount) : amount{ _amount } {};

    double amount;
};

void display_balance(const Money balance)
{
    std::cout << "The balance is: " << balance.amount << std::endl;
}

int main(int argc, char* argv[])
{
    Money payable{ 79.99 };        // Legal: direct initialization is explicit.

    display_balance(payable);      // Legal: no conversion required
    display_balance(49.95);        // Error: no suitable conversion exists to convert from double to Money.
    display_balance((Money)9.99f); // Legal: explicit cast to Money

    return 0;
}

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

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

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

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

#include <iostream>

class Money
{
public:
    Money() : amount{ 0.0 } {};
    Money(double _amount) : amount{ _amount } {};

    operator double() const { return amount; }
private:
    double amount;
};

void display_balance(const Money balance)
{
    std::cout << "The balance is: " << balance << std::endl;
}

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

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

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

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

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

    operator struct String { char string_storage; }() // illegal
    
  • Функции преобразования не принимают аргументов. Указание любых параметров в объявлении является ошибкой.

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

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

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

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

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

#include <iostream>

class Money
{
public:
    Money() : amount{ 0.0 } {};
    Money(double _amount) : amount{ _amount } {};

    explicit operator double() const { return amount; }
private:
    double amount;
};

void display_balance(const Money balance)
{
    std::cout << "The balance is: " << (double)balance << std::endl;
}

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