Преобразования

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

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

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

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

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

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

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

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

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

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

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

  • Множественное наследование. Преобразование определено в нескольких базовых классах. Для получения дополнительной информации см. Неоднозначность.

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

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

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

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

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

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

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

Ключевое слово 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 преобразование требуется, так как тип аргумента, double со значением 49,95, не относится к ожидаемому функцией. Функция не может использовать это значение напрямую, однако поскольку выполняется преобразование из типа аргумента (double) в тип соответствующего параметра (Money), временное значение типа Money создается на основе аргумента и используется для завершения вызова функции. Обратите внимание, что в третьем вызове функции display_balance аргумент относится не к типу double, а к типу float со значением 9,99 и, тем не менее, вызов функции можно завершить, так как компилятор может выполнить стандартное преобразование (в этом случае из float в double), а затем пользовательское преобразование из double в Money, чтобы завершить необходимое преобразование.

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

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

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

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

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

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

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

Объявление конструктора преобразования как 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, однако обратите внимание, что компилятор снова "помог" выполнить приведение, вставив неявное приведение из float в double.

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

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

Функции преобразования определяют преобразования из пользовательского в другие типы. Эти функции иногда называют "операторами приведения", так как они, наряду с конструкторами преобразования, вызываются, когда значение приводится к другому типу. В следующем примере показана функция преобразования, которая преобразует пользовательский тип 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 направляется в стандартный вывод с помощью оператора вставки в поток <<. Поскольку оператор вставки в поток не определен для пользовательского типа Money, но имеется для встроенного типа double, компилятор может использовать функцию преобразования Money в double, чтобы удовлетворить требования оператора вставки в поток.

Функции преобразования наследуются производными классами. Функции преобразования в производном классе переопределяют наследуемую функцию преобразования, только когда выполняют преобразование в точно такой же тип. Например, пользовательская функция преобразования производного класса operator int не переопределяет (и даже вообще не влияет) на пользовательскую функцию базового класса operator short, даже если стандартное преобразование определяет отношение преобразования между 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;
}

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

См. также

Ссылки

Специальные функции-члены (C++)