13.14 – Конструктор преобразования, explicit и delete
По умолчанию C++ обрабатывает любой конструктор как оператор неявного преобразования. Рассмотрим следующий случай:
#include <cassert>
#include <iostream>
class Fraction
{
private:
int m_numerator;
int m_denominator;
public:
// Конструктор по умолчанию
Fraction(int numerator = 0, int denominator = 1) :
m_numerator(numerator), m_denominator(denominator)
{
assert(denominator != 0);
}
// Конструктор копирования
Fraction(const Fraction ©) :
m_numerator(copy.m_numerator), m_denominator(copy.m_denominator)
{
// здесь нет необходимости проверять знаменатель на 0,
// поскольку copy уже должна быть корректным объектом Fraction
std::cout << "Copy constructor called\n"; // просто чтобы доказать, что это работает
}
friend std::ostream& operator<<(std::ostream& out, const Fraction &f1);
int getNumerator() { return m_numerator; }
void setNumerator(int numerator) { m_numerator = numerator; }
};
void printFraction(const Fraction &f)
{
std::cout << f;
}
std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
out << f1.m_numerator << "/" << f1.m_denominator;
return out;
}
int main()
{
printFraction(6);
return 0;
}
Хотя функция printFraction()
ожидает объект Fraction
, вместо этого мы передали ей целочисленный литерал 6. Поскольку у Fraction
есть конструктор, который принимает одно число int
, компилятор неявно преобразует литерал 6 в объект Fraction
. Для этого он инициализирует параметр f
функции printFraction()
с помощью конструктора Fraction(int, int)
.
Следовательно, показанная выше программа печатает:
6/1
Это неявное преобразование работает для всех видов инициализации (прямой, унифицированной и копирующей).
Конструкторы, которые могут использоваться для неявных преобразований, называются конструкторами преобразования (или преобразующими конструкторами). До C++11 только конструкторы, принимающие один параметр, могли быть конструкторами преобразования. Однако с новым синтаксисом унифицированной инициализации в C++11 это ограничение было снято, и конструкторы, принимающие несколько параметров, теперь также могут быть конструкторами преобразования.
Ключевое слово explicit
Хотя выполнение неявных преобразований имеет смысл в случае с Fraction
, в других случаях это может быть нежелательно или привести к неожиданному поведению:
#include <string>
#include <iostream>
class MyString
{
private:
std::string m_string;
public:
MyString(int x) // размещаем строку размером x
{
m_string.resize(x);
}
MyString(const char *string) // размещаем строку для хранения значения string
{
m_string = string;
}
friend std::ostream& operator<<(std::ostream& out, const MyString &s);
};
std::ostream& operator<<(std::ostream& out, const MyString &s)
{
out << s.m_string;
return out;
}
void printString(const MyString &s)
{
std::cout << s;
}
int main()
{
MyString mine = 'x'; // будет компилироваться и использовать MyString(int)
std::cout << mine << '\n';
printString('x'); // будет компилироваться и использовать MyString(int)
return 0;
}
В приведенном выше примере пользователь пытается инициализировать строку с помощью char
. Поскольку char
является частью семейства целочисленных типов, компилятор будет использовать конструктор преобразования MyString(int)
, чтобы неявно преобразовать char
в MyString
. Затем программа напечатает этот объект MyString
, что приведет к неожиданным результатам. Точно так же вызов printString('x')
вызывает неявное преобразование, которое приводит к той же проблеме.
Один из способов решения этой проблемы – сделать конструкторы (и функции преобразования) явными с помощью ключевого слова explicit
, которое помещается перед именем функции. Явные конструкторы и функции преобразования не будут использоваться для неявных преобразований или копирующей инициализации:
#include <string>
#include <iostream>
class MyString
{
private:
std::string m_string;
public:
// ключевое слово explicit делает этот конструктор
// непригодным для неявных преобразований
explicit MyString(int x) // размещаем строку размером x
{
m_string.resize(x);
}
// размещаем строку для хранения значения string
MyString(const char *string)
{
m_string = string;
}
friend std::ostream& operator<<(std::ostream& out, const MyString &s);
};
std::ostream& operator<<(std::ostream& out, const MyString &s)
{
out << s.m_string;
return out;
}
void printString(const MyString &s)
{
std::cout << s;
}
int main()
{
// ошибка компиляции, так как MyString(int) теперь явный
// и ничто не будет соответствовать этому коду
MyString mine = 'x';
std::cout << mine;
// ошибка компиляции, поскольку MyString(int) не может
// использоваться для неявных преобразований
printString('x');
return 0;
};
Приведенная выше программа не будет компилироваться, так как MyString(int)
был сделан явным, и не удалось найти соответствующий конструктор преобразования для неявного преобразования 'x' в MyString
.
Однако обратите внимание, что создание явного конструктора предотвращает только неявные преобразования. Явные преобразования (через приведение типа) по-прежнему разрешены:
// Допустимо: явное приведение 5 к MyString(int)
std::cout << static_cast<MyString>(5);
Прямая и унифицированная инициализации также по-прежнему преобразуют параметры для соответствия (унифицированная инициализация не приведет к сужающим преобразованиям, но с радостью выполнит другие типы преобразований).
// Допустимо: параметры инициализации все еще могут быть
// неявно преобразованы для соответствия
MyString str{'x'};
Правило
Подумайте о том, чтобы сделать ваши конструкторы и пользовательские функции-члены преобразования явными, чтобы предотвратить ошибки неявного преобразования.
Ключевое слово delete
В нашем случае с MyString
мы на самом деле хотим полностью запретить преобразование 'x' в MyString
(явное или неявное, поскольку результаты не будут интуитивно понятными). Один из способов частично сделать это – добавить конструктор MyString(char)
и сделать его закрытым:
#include <string>
#include <iostream>
class MyString
{
private:
std::string m_string;
// объекты типа MyString(char) не могут быть созданы извне класса
MyString(char)
{
}
public:
// ключевое слово explicit делает этот конструктор
// непригодным для неявных преобразований
explicit MyString(int x) // размещаем строку размером x
{
m_string.resize(x);
}
// размещаем строку для хранения значения string
MyString(const char *string)
{
m_string = string;
}
friend std::ostream& operator<<(std::ostream& out, const MyString &s);
};
std::ostream& operator<<(std::ostream& out, const MyString &s)
{
out << s.m_string;
return out;
}
int main()
{
MyString mine('x'); // ошибка компиляции, поскольку MyString(char)
// является закрытым
std::cout << mine;
return 0;
}
Однако этот конструктор по-прежнему можно использовать изнутри класса (закрытый доступ мешает только нечленам класса вызывать эту функцию).
Лучший способ решить проблему – использовать ключевое слово delete
(введенное в C++11) для удаления функции:
#include <string>
#include <iostream>
class MyString
{
private:
std::string m_string;
public:
// любое использование этого конструктора является ошибкой
MyString(char) = delete;
// ключевое слово explicit делает этот конструктор
// непригодным для неявных преобразований
explicit MyString(int x) // размещаем строку размером x
{
m_string.resize(x);
}
// размещаем строку для хранения значения string
MyString(const char *string)
{
m_string = string;
}
friend std::ostream& operator<<(std::ostream& out, const MyString &s);
};
std::ostream& operator<<(std::ostream& out, const MyString &s)
{
out << s.m_string;
return out;
}
int main()
{
MyString mine('x'); // ошибка компиляции,
// т.к. MyString(char) удален
std::cout << mine;
return 0;
}
Когда функция была удалена, любое использование этой функции считается ошибкой компиляции.
Обратите внимание, что конструктор копирования и перегруженные операторы также могут быть удалены, чтобы предотвратить их использование.