13.14 – Конструктор преобразования, explicit и delete

Добавлено 21 июля 2021 в 16:12
Глава 13 – Перегрузка операторов  (содержание)

По умолчанию 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 &copy) :
        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;
}

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

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

Теги

C++ / CppdeleteexplicitLearnCppДля начинающихКонструктор / Constructor / ctor (программирование)Конструктор преобразованияОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.