Перегрузка операторов C++, часть четвёртая - преобразования типов

newbie_

Опубликован:  2020-03-04T05:35:30.003182Z
Отредактирован:  2020-03-04T05:32:33.573126Z
3000
Продолжаем изучать C++ и его возможности. В очередной демонстрации рассмотрим преобразования типов и сосредоточим внимание на соответствующих инструментах C++ - конструкторах классов и служебных функциях operator, позволяющих реализовать простые механизмы преобразования типов в коде. Все инструменты я продемонстрирую на простых учебных примерах из книги Роберта Лафоре.

1. Общие сведения

Как известно, C++ имеет строгую статическую типизацию, при этом он наследует базовые типы языка C (int, float, double etc.) и даёт возможность разрабатывать собственные пользовательские типы данных посредством декларации и определения классов.

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

float a, b;
int c = 4;
a = 1.25F;
b = a * c

В выражении b = a * c переменные a и b имеют тип float, а переменная c имеет тип int и при компиляции будет автоматически преобразована в тип float. В данном случае имеет место неявное преобразование типов, и то, каким образом такое преобразование будет осуществляться, определено в коде компилятора. Не всегда ожидания программиста совпадают с реальными действиями компилятора, поэтому предусмотрено явное преобразование типов, которое кроме всего прочего помогает избежать некоторых каверзных ситуаций.

b = a * static_cast<float>(c);

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

Все примеры этого обзора я продемонстрирую с помощью своего десктопа с Debian buster на борту и свободного компилятора g++.

NZpzRHvbpA.png

2. Преобразования объектов и базовых типов

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

ScxtjwKyzr.png

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

// mconv.cpp

#include <iostream>

Декларирую класс Distance, который будет определять объекты пользовательского типа.

class Distance
{
private:
  const float MTF;
  int feet;
  float inches;
public:
  // Конструктор без аргументов определяет состояние вновь созданных
  // объявлением без инициализации объектов и их свойств
  Distance() : MTF(3.280833F), feet(0), inches(0.0F) {}
  // Конструктор с одним аргументом определяет
  // явное и неявное преобразование переменной базового типа float
  // в экземпляр класса Distance
  Distance(float meters) : MTF(3.280833F)
  {
    float fltfeet = MTF * meters;
    feet = static_cast<int>(fltfeet);
    inches = 12.0F * (fltfeet - static_cast<float>(feet));
  }
  // Конструктор с двумя аргументами определяет состояние
  // вновь созданных объектов объявлением с инициализацией
  Distance(int f, float i) : MTF(3.280833F), feet(f), inches(i) {}
  // Функция getdist() даёт возможность инициировать
  // созданный объект с запросом данных у пользователя программы,
  // в данной реализации программы использоваться не будет
  void getdist()
  {
    std::cout << "Enter feet: ";
    std::cin >> feet;
    std::cout << "Enter inches: ";
    std::cin >> inches;
  }
  // Функция showdist() выведет на экран значение объекта в
  // заданной форме
  void showdist() const { std::cout << feet << "\'-" << inches << "\"\n"; }
  // Функция operator float() определяет преобразование экземпляра
  // класса Distance в переменную базового типа float 
  operator float() const
  {
    float fracfeet = inches / 12.0F;
    fracfeet += static_cast<float>(feet);
    return fracfeet / MTF;
  }
};

В коде класса Distance предусмотрено два инструмента для преобразования типов:

  1. Конструктор с одним аргументом определяет преобразование переменной базового типа float в экземпляр класса Distance;
  2. Функция operator float() определяет преобразование экземпляра класса Distance в переменную базового типа float.

Давайте посмотрим, как это работает, определяю функцию main программы.

int main()
{
  float mtrs;
  // Тестируем конструктор класса Distance
  // с одним аргументом, создаём экземпляр dist1
  // и преобразуем при его создании
  // метры (2.35) в футы и дюймы - неявное преобразование
  Distance dist1 = 2.35F;
  // Выводим на экран значение dist1 в заданной форме
  // в футах и дюймах
  std::cout << "dist1 = ";
  dist1.showdist();
  // Преобразуем значения переменной dist1
  // в переменную базового типа float с помощью соответствующей
  // функции operator float() - явное преобразование
  mtrs = static_cast<float>(dist1);
  // Выводим на экран значение переменной dist1
  // в метрах, ожидаем увидеть 2.35 метра
  std::cout << "dist1 = " << mtrs << " meters\n";
  // Тестируем конструктор класса Distance с двумя аргументами,
  // создаём экземпляр класса Distance
  Distance dist2(7, 8.51949F);
  // Преобразуем значения этого экземпляра в
  // переменную базового типа float - неявное преобразование
  // опять работает функция operator float()
  mtrs = dist2;
  // Выводим на экран значение переменной dist1
  // в метрах, ожидаем 2.35 метра
  std::cout << "dist2 = " << mtrs << " meters\n";
}

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

g++ -o exe -Wall mconv.cpp
./exe

bkafe7B6oG.png

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

3. Преобразования объектов и строк

Рассмотрим второй пример преобразования - объект в строку и обратно, как нельзя кстати пригодится рассмотренный в одном из предыдущих выпусков блога класс String. Создаю новый файл с именем sconv.cpp.

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

// sconv.cpp

#include <iostream>
#include <cstring>

Декларирую класс String.

class String
{
private:
  static const int SZ = 80;
  char str[SZ];
public:
  // Конструктор без аргументов определяет состояние
  // объявленных без инициализации экземпляров этого класса
  String() { str[0] = '\n'; }
  // Первый конструктор с одним аргументом принимает
  // строковый массив и копирует его в соответствующее
  // свойство экземпляра - преобразование строкового массива в объект
  String(char s[]) { strcpy(str, s); }
  // Второй конструктор с одним аргументом принимает
  // строковую константу и копирует её в соответствующее
  // свойство экземпляра - преобразование строковой константы в объект
  String(const char * s) { strcpy(str, s); }
  // Функция display() выводит на терминал
  // значение экземпляра в заданной форме
  void display() const { std::cout << str; }
  // Функция operator char*() определяет
  // механизм преобразование экземпляра в строку
  operator char*() { return str; }
};

Класс String имеет два конструктора с одним аргументом. В первом случае экземпляр класса копирует строковый массив, а во втором случае - строковую константу. Оба конструктора осуществляют преобразование базового типа в экземпляр. Кроме этого класс String имеет функцию operator char*(), с помощью которой можно выполнить обратное преобразование. Протестируем функционал класса String с помощью функции main.

int main()
{
  // Объявляем экземпляр класса
  String s1;
  // Объявляем и инициализируем строковый массив
  char xstr[] = "Lorem Ipsum Dolor! ";
  // Копируем данные строкового массива в экземпляр
  // класса String - работает первый конструктор с одним аргументом
  s1 = xstr;
  // Выводим значение экземпляра s1 на экран терминала
  s1.display();
  // Объявляем второй экземпляр класса String
  // и инициализируем его с помощью строковой константы
  // работает второй конструктор с одним аргументом
  String s2 = "Proin Consectetur Mauris.";
  // Преобразуем экземпляр s2 в строку
  // и выводим её на экран терминала - работает функция operator char*()
  std::cout << static_cast<char*>(s2) << std::endl;
  return 0;
}

Компилирую программу и пробую её исполнить.

2tHlHPdXe8.png

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

4. Взаимное преобразование двух объектов

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

  1. Механизм преобразования определён в исходном объекте, который преобразуют в другой объект;
  2. Механизм преобразования определён в объекте, в который преобразуют исходный объект.

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

  • 24-часовой формат, когда текущее время отображает часы, минуты и секунды разделенные двоеточием - 11:45:38;
  • 12-часовой формат, когда текущее время отображает часы, минуты и обозначение a.m. или p.m. - 11:15a.m..

Рассмотрим программу, которая преобразует 24-часовой формат отображения текущего времени в 12-часовой формат. Создаю новый файл - tconv1.cpp. Для реализации задуманного мне необходимы два заголовочных файла стандартной библиотеки.

// tconv1.cpp

#include <iostream>
#include <string>

Для представления времени в 12-часовом формате создаю класс time12.

class time12
{
private:
  bool pm;
  int hrs;
  int mins;
public:
  time12() : pm(true), hrs(0), mins(0) {}
  time12(bool ap, int h, int m) : pm(ap), hrs(h), mins(m) {}
  void display() const;
};

Одна из функций этого класса требует дополнительного определения.

void time12::display() const
{
  std::cout << hrs << ":";
  if (mins < 10) std::cout << '0';
  std::cout << mins << ' ';
  std::string am_pm = pm ? "p.m." : "a.m.";
  std::cout << am_pm << std::endl;
}

Второй класс - time24 - будет определять представление текущего времени в 24-часовом формате.

class time24
{
private:
  int hours;
  int minutes;
  int seconds;
public:
  time24() : hours(0), minutes(0), seconds(0) {}
  time24(int h, int m, int s) : hours(h), minutes(m), seconds(s) {}
  void display() const;
  // Функция operator time12() определяет механизм преобразования
  // экземпляра time24 в экземпляр time12
  operator time12() const;
};

Как видно, механизм преобразования объектов time24 в объекты time12 заложен в исходном объекте - time24. Определяем этот механизм и функцию display.

void time24::display() const
{
  if (hours < 10) std::cout << '0';
  std::cout << hours << ':';
  if (minutes < 10) std::cout << '0';
  std::cout << minutes << ':';
  if (seconds < 10) std::cout << '0';
  std::cout << seconds << std::endl;
}

time24::operator time12() const
{
  int hrs24 = hours;
  bool pm = hours < 12 ? false : true;
  int roundMins = seconds < 30 ? minutes : minutes + 1;
  if (roundMins == 60)
  {
    roundMins = 0;
    ++hrs24;
    if (hrs24 == 12 || hrs24 == 24)
      pm = (pm == true) ? false : true;
  }
  int hrs12 = (hrs24 < 13) ? hrs24 : hrs24 - 12;
  if (hrs12 == 0)
  {
    hrs12 = 12;
    pm = false;
  }
  return time12(pm, hrs12, roundMins);
}

Посмотрим, как это работает, определяю функцию main.

int main()
{
  // Объявляем переменные h, m, s
  int h, m, s;
  while (true)
  {
    // Запрашиваем у пользователя ввод показаний часов, минут и секунд
    std::cout << "Enter 24-hour time:\n";
    std::cout << "   Hours (0 to 23): ";
    std::cin >> h;
    // Если пользователь ввёл показания часов больше 23
    if (h > 23)
      // Прерываем исполнение программы
      return 1;
    std::cout << "   Minutes:         ";
    std::cin >> m;
    std::cout << "   Seconds:         ";
    std::cin >> s;
    // С помощью полученных от пользователя данных
    // создаём экземпляр класса time24
    time24 t24(h, m, s);
    // Для контроля выводим значения полученного экземпляра
    // на терминал в заданной форме
    std::cout << "You entered: ";
    t24.display();
    // Создаём экземпляр класса time12 и преобразуем в него
    // исходный объект time24 - работает функция operator time12()
    // определённая в исходном объекте (time24)
    time12 t12 = t24;
    // Выводим на экран терминала полученный объект
    // в заданной форме
    std::cout << "12-hour time: ";
    t12.display();
    std::cout << std::endl;
  }
  return 0;
}

Компилирую этот код и исполняю полученную программу.

g++ -o exe -Wall tconv1.cpp
./exe

VHdMnxi21P.png

Как видно на снимке экрана выше, компилятор отработал без ошибок и предупреждений. При исполнении программы введённые пользователем часы, минуты и секунды были успешно преобразованы в 12-часовой формат. Здесь следует иметь ввиду, что введённые пользователем данные никак не проверяются, и, если пользователь введёт например 22:87:76, то может получиться каверза. Для этого надо бы усовершенствовать соответствующий конструктор класса time24 - но это не цель настоящего обзора. А вот преобразование объекта time24 в объект time12 вполне сносно работает.

Рассмотрим второй вариант этой же программы, и на этот раз весь функционал преобразования объекта time24 в объект time12 определим в объекте time12. Создаю новый файл - tconv2.cpp. Для этого варианта программы мне понадобятся те же заголовочные файлы.

// tconv2.cpp

#include <iostream>
#include <string>

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

class time24
{
private:
  int hours;
  int minutes;
  int seconds;
public:
  time24() : hours(0), minutes(0), seconds(0) {}
  time24(int h, int m, int s) : hours(h), minutes(m), seconds(s) {}
  void display() const;
  // Три дополнительные функции, которые будут использованы
  // для реализации механизма преобразования объектов
  int getHrs() const { return hours; }
  int getMins() const { return minutes; }
  int getSec() const { return seconds; }
};

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

class time12
{
private:
  bool pm;
  int hrs;
  int mins;
public:
  time12() : pm(true), hrs(0), mins(0) {}
  time12(bool ap, int h, int m) : pm(ap), hrs(h), mins(m) {}
  // Дополнительный конструктор в котором будет реализован механизм
  // преобразования объектов
  time12(time24 t);
  void display() const;
};

Определение дополнительного конструктор класса time12 примет следующий вид.

time12::time12(time24 t)
{
  int hrs24 = t.getHrs();
  pm = t.getHrs() < 12 ? false : true;
  mins = (t.getSec() < 30) ? t.getMins() : t.getMins() + 1;
  if (mins == 60)
  {
    mins = 0;
    ++hrs24;
    if (hrs24 == 12 || hrs24 == 24)
      pm = (pm == true) ? false : true;
  }
  hrs = (hrs24 < 13) ? hrs24 : hrs24 - 12;
  if (hrs == 0)
  {
    hrs = 12;
    pm = false;
  }
}

В этом конструкторе для получения данных из объекта time24 использованы методы getHrs, getMins, getSec.

Функции display обоих классов и функция main остались прежними, копирую их без изменений из первого варианта программы. Компилирую программу и пробую её исполнить.

g++ -o exe -Wall tconv2.cpp
./exe

QVHuy78DuE.png

Результат исполнения программы не изменился, взаимное преобразование объектов работает и в этом варианте. Возникает резонный вопрос, какой из двух вариантов использовать? Ответ на этот вопрос весьма неоднозначен и зависит от нескольких обстоятельств. Если оба класса разрабатываются вами, то можно использовать любой из них, а вот если один из классов является заимствованным из какой-либо библиотеки, то второй вариант конверсии будет единственным возможным.

5. Явное и неявное преобразование типов

Вернемся к нашему классу Distance. Представим ситуацию, когда неявное преобразование базового типа в объект необходимо предотвратить, но при этом сохранить инициализацию экземпляра класса с помощью конструктора с одним аргументом. Создаю новый файл - econv.cpp, и пишу в нём следующий код.

// explicit.cpp

#include <iostream>

void fancyDist(Distance d);

class Distance
{
private:
  const float MTF;
  int feet;
  float inches;
public:
  Distance() : MTF(3.280833F), feet(0), inches(0) {}
  // Конструктор с одним аргументом, с помощью которого
  // осуществляется преобразование float в экземпляр
  Distance(float meters) : MTF(3.280833F)
  {
    float fltfeet = MTF * meters;
    feet = static_cast<int>(fltfeet);
    inches = 12.0F * (fltfeet - static_cast<float>(feet));
  }
  void showdist() { std::cout << feet << "\'-" << inches << "\"\n"; }
};

int main()
{
  // Создаём экземпляр и инициализируем его с помощью
  // конструктора с одним аргументом
  Distance dist1(2.35F);
  dist1.showdist();
  // Создаём объект путём неявного преобразования типов
  // этот вариант использования необходимо предотвратить
  Distance dist2 = 2.35F;
  std::cout << "dist2 = ";
  dist2.showdist();
  // Создаём переменную типа float
  float mtrs = 2.35F;
  std::cout << "dist3 ";
  // Неявное преобразование float в экземпляр класса Distance
  // этот вариант использования тоже необходимо предотвратить
  fancyDist(mtrs);
  return 0;
}

void fancyDist(Distance d)
{
  std::cout << "(in feet and inches) = ";
  d.showdist();
}

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

zdlNtGXy9I.png

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

XtI7iff1Zj.png

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

Комментарии: