Ввод/вывод через <iostream> и <cstdio> // FAQ C++

Добавлено 20 сентября 2020 в 01:00

Почему я должен использовать <iostream> вместо традиционного <cstdio>?

Чтобы повысить безопасность типов, уменьшить количество ошибок, обеспечить расширяемость и наследуемость.

printf(), возможно, не сломан, и scanf(), возможно, пригоден для жизни, несмотря на склонность к ошибкам, однако они оба ограничены в отношении того, что может делать ввод/вывод C++. Ввод/вывод C++ (с использованием << и >>) относительно C (с использованием printf() и scanf()):

  • более безопасен: с <iostream> тип объекта, для которого выполняется ввод/вывод, статически известен компилятору. <cstdio>, напротив, использует поля "%" для динамического определения типов;
  • менее подвержен ошибкам: с <iostream> нет избыточных токенов "%", которые должны согласовываться с реальными объектами, выполняющими ввод/вывод. Удаление избыточности удаляет класс ошибок;
  • расширяем: механизм C++ <iostream> позволяет вводить/выводить новые определяемые пользователем типы без нарушения существующего кода. Представьте себе хаос, если бы все одновременно добавляли новые несовместимые поля "%" в printf() и scanf()?!
  • наследуем: механизм C++ <iostream> построен из реальных классов, таких как std::ostream и std::istream. В отличие от FILE* в <cstdio>, это настоящие классы и, следовательно, наследуемые. Это означает, что у вас могут быть другие определяемые пользователем штуковины, которые выглядят и действуют как потоки, но при этом делают все странные и чудесные вещи, которые вы захотите. Вы автоматически получаете возможность использовать миллионы строк кода ввода/вывода, написанные пользователями, которых вы даже не знаете, и им не нужно знать о вашем классе «расширенного потока».

Почему моя программа заходит в бесконечный цикл, когда кто-то вводит недопустимый входной символ?

Например, предположим, что у вас есть следующий код, который считывает целые числа из std::cin:

#include <iostream>

int main()
{
  std::cout << "Enter numbers separated by whitespace (use -1 to quit): ";
  int i = 0;
  while (i != -1) {
    std::cin >> i;        // ПЛОХО — смотри комментарии ниже
    std::cout << "You entered " << i << '\n';
  }
  // ...
}

Проблема с этим кодом в том, что в нем отсутствует какая-либо проверка, чтобы увидеть, ввел ли кто-то недопустимый вводимый символ. В частности, если кто-то вводит что-то, что не похоже на целое число (например, 'x'), поток std::cin переходит в «состояние ошибки», и все последующие попытки ввода немедленно возвращаются без каких-либо действий. Другими словами, программа входит в бесконечный цикл; если последним успешно прочитанным числом было 42, программа будет снова и снова печатать сообщение «You entered 42».

Простой способ проверить на недопустимость ввода – переместить запрос ввода из тела цикла while в управляющее выражение цикла while. Например,

#include <iostream>

int main()
{
  std::cout << "Enter a number, or -1 to quit: ";
  int i = 0;
  while (std::cin >> i) {    // ХОРОШО
    if (i == -1) break;
    std::cout << "You entered " << i << '\n';
  }
  // ...
}

Это приведет к завершению цикла while либо при достижении конца файла, либо при вводе некорректного целого числа, либо при вводе -1.

(Естественно, вы можете устранить break, изменив выражение цикла while с while (std::cin >> i) на while ((std::cin >> i) && (i != -1)), но на самом деле в данном FAQ это не важно, поскольку данный раздел FAQ посвящен iostreams, а не общим руководствам по структуре программ.)


Как я могу заставить std::cin пропускать недопустимые входные символы?

Используйте std::cin.clear() и std::cin.ignore().

#include <iostream>
#include <limits>

int main()
{
  int age = 0;
  while ((std::cout << "How old are you? ")
         && !(std::cin >> age)) {
    std::cout << "That's not a number; ";
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  }
  std::cout << "You are " << age << " years old\n";
  // ...
}

Конечно, вы также можете вывести сообщение об ошибке, когда введенное значение не попадает в допустимый диапазон. Например, если вы хотите, чтобы возраст был от 1 до 200, вы можете изменить цикл while на:

  // ...
  while ((std::cout << "How old are you? ")
         && (!(std::cin >> age) || age < 1 || age > 200)) {
    std::cout << "That's not a number between 1 and 200; ";
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  }
  // ...

Ниже показан пример выполнения данного кода:

How old are you? foo
That's not a number between 1 and 200; How old are you? bar
That's not a number between 1 and 200; How old are you? -3
That's not a number between 1 and 200; How old are you? 0
That's not a number between 1 and 200; How old are you? 201
That's not a number between 1 and 200; How old are you? 2
You are 2 years old

Как работает этот напуганный синтаксис while (std::cin >> foo)?

Пример использования этого «напуганного синтаксиса while (std::cin >> foo)» смотрите в ответе на предыдущий вопрос.

Выражение (std::cin >> foo) вызывает соответствующий оператор operator>> (например, оно вызывает operator>>, который принимает std::istream слева и, если foo имеет тип int, int& справа). Функции operator>>std::istream по соглашению возвращают свой левый аргумент, что в данном случае означает, что будет возвращен std::cin. Затем компилятор замечает, что возвращенный std::istream находится в логическом контексте, поэтому он преобразует этот std::istream в логическое значение.

Чтобы преобразовать std::istream в логическое значение, компилятор вызывает функцию-член std::istream::operator void*(). Она возвращает указатель void*, который, в свою очередь, преобразуется в логическое значение (NULL становится false, любой другой указатель становится true). Итак, в этом случае компилятор генерирует вызов std::cin.operator void*(), как если бы вы выполнили его явное приведение, например, (void*) std::cin.

Оператор приведения void*() возвращает некоторый указатель, отличный от NULL, если поток находится в хорошем состоянии, или NULL, если он находится в состоянии сбоя. Например, если вы выполняете чтение слишком много раз (например, если вы уже находитесь в конце файла), или если реальная информация во входном потоке недопустима для типа foo (например, если foo имеет тип int, а данные представляют собой символ 'x'), поток перейдет в состояние ошибки, а оператор приведения вернет NULL.

Причина, по которой operator>> просто не возвращает логическое значение (или void*), указывающее, успешен он или нет, – это поддержка «каскадного» синтаксиса:

std::cin >> foo >> bar;

operator>> является левоассоциативным, что означает, что приведенный выше код разбирается как:

(std::cin >> foo) >> bar;

Другими словами, если мы заменим operator>> на имя обычной функции, такое как readFrom(), это станет выражением:

readFrom( readFrom(std::cin, foo), bar);

Как всегда, мы начинаем вычислять с самого внутреннего выражения. Из-за левой ассоциативности оператора operator>> это самое левое выражение будет std::cin >> foo. Следующему выражению это выражение возвращает std::cin (точнее, возвращает ссылку на свой левый аргумент). Следующее выражение также возвращает (ссылку на) std::cin, но эта вторая ссылка игнорируется, поскольку это самое внешнее выражение в этом «операторе выражения».


Почему мой ввод обрабатывается после конца файла?

Поскольку состояние eof не может быть установлено до тех пор, пока не будет предпринята попытка чтения за пределами конца файла. То есть чтение последнего байта из файла может не установить состояние eof. Например, предположим, что входной поток сопоставлен с клавиатурой – в этом случае для библиотеки C++ даже теоретически невозможно предсказать, будет ли символ, который только что набрал пользователь, последним символом.

Например, в следующем коде может быть ошибка на единицу со счетчиком i:

int i = 0;
while (! std::cin.eof()) {   // НЕПРАВИЛЬНО! (ненадежно)
  std::cin >> x;
  ++i;
  // Работа с x ...
}

Что вам действительно нужно:

int i = 0;
while (std::cin >> x) {      // ПРАВИЛЬНО! (надежно)
  ++i;
  // Работа с x ...
}

Почему моя программа игнорирует мой запрос на ввод после первой итерации?

Потому что извлекатель чисел оставляет во входном буфере нецифровые значения.

Если ваш код выглядит так:

char name[1000];
int age;
for (;;) {
  std::cout << "Name: ";
  std::cin >> name;
  std::cout << "Age: ";
  std::cin >> age;
}

Что вам действительно нужно:

for (;;) {
  std::cout << "Name: ";
  std::cin >> name;
  std::cout << "Age: ";
  std::cin >> age;
  std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

Конечно, вы можете изменить оператор for (;;) на while (std::cin), но не путайте это с пропуском нечисловых символов в конце цикла с помощью строки std::cin.ignore(...);.


Должен ли я заканчивать выходные строки с помощью std::endl или '\n'?

Использование std::endl очищает выходной буфер после отправки '\n', что означает более высокую производительность std::endl. Очевидно, если вам нужно очистить буфер после отправки '\n', используйте std::endl; но если вам не нужно очищать буфер, код будет работать быстрее, если вы используете '\n'.

Этот код просто выводит '\n':

void f()
{
  std::cout << /*...stuff...*/ << '\n';
}

Этот код выводит '\n', затем очищает выходной буфер:

void g()
{
  std::cout << /*...stuff...*/ << std::endl;
}

Этот код просто очищает выходной буфер:

void h()
{
  std::cout << /*...stuff...*/ << std::flush;
}

Примечание: для всех трех приведенных выше примеров требуется #include <iostream>.


Как я могу обеспечить печать для моего класса Fred?

Используйте перегрузку операторов, чтобы предоставить дружественный оператор сдвига влево, operator<<.

#include <iostream>
class Fred {
public:
  friend std::ostream& operator<< (std::ostream& o, const Fred& fred);
  // ...
private:
  int i_;    // Просто для демонстрации
};

std::ostream& operator<< (std::ostream& o, const Fred& fred)
{
  return o << fred.i_;
}

int main()
{
  Fred f;
  std::cout << "My Fred object: " << f << "\n";
  // ...
}

Мы используем функцию, не являющуюся членом, (в данном случае она является дружественной), поскольку объект Fred является правым операндом оператора <<. Если объект Fred должен был находиться слева от << (то есть myFred << std::cout, а не std::cout << myFred), мы могли бы использовать функцию, член класса, с именем operator<< .

Обратите внимание, что operator<< возвращает поток. Это сделано для того, чтобы операции вывода могли быть каскадными.


Но разве я не должен всегда использовать метод printOn(), а не дружественную функцию?

Нет.

Обычная причина, по которой люди хотят всегда использовать метод printOn(), а не дружественную функцию, заключается в том, что они ошибочно полагают, что друзья нарушают инкапсуляцию, и/или что друзья – это зло. Эти убеждения наивны и ошибочны: при правильном использовании друзья на самом деле могут улучшить инкапсуляцию.

Это не означает, что метод printOn() никогда не бывает полезным. Например, он полезен при обеспечении печати для всей иерархии классов. Но если вы используете метод printOn(), он обычно должен быть protected, а не public.

Для полноты, вот «подход с методом printOn()». Идея состоит в том, чтобы иметь функцию-член, часто называемую printOn(), которая выполняет реальную печать, а затем operator<< вызывает этот метод printOn(). Когда это сделано неправильно, метод printOn() становится общедоступным (public), поэтому operator<< не обязательно должен быть дружественным (friend) – это может быть простая функция верхнего уровня, которая не является ни другом, ни членом класса. Вот пример кода:

#include <iostream>
class Fred {
public:
  void printOn(std::ostream& o) const;
  // ...
};

// operator<< может быть объявлен не-другом [НЕ рекомендуется!]
std::ostream& operator<< (std::ostream& o, const Fred& fred);

// Реальная печать выполняется внутри метода printOn() [НЕ рекомендуется!]
void Fred::printOn(std::ostream& o) const
{
  // ...
}

// operator<< вызывает printOn() [НЕ рекомендуется!]
std::ostream& operator<< (std::ostream& o, const Fred& fred)
{
  fred.printOn(o);
  return o;
}

Люди ошибочно полагают, что это снижает затраты на поддержку, «поскольку позволяет избежать дружественной функции». Это неправильное предположение, потому что:

  1. Подход «член вызывается функцией верхнего уровня» имеет нулевую выгоду с точки зрения затрат на поддержку. Допустим, для фактической печати требуется N строк кода. В случае дружественной функции эти N строк кода будут иметь прямой доступ к частям private/protected класса, что означает, что всякий раз, когда кто-то изменяет private/protected части класса, эти N строк кода необходимо будет просканировать и, возможно, изменить, что увеличивает стоимость поддержки. Однако использование метода printOn() ничего не меняет: у нас по-прежнему есть N строк кода, которые имеют прямой доступ к private/protected частям класса. Таким образом, перенос кода из дружественной функции в функцию-член вообще не снижает затрат на обслуживание. Нулевое снижение. В стоимости поддержки нет выгоды. (Во всяком случае, с методом printOn() это немного хуже, поскольку теперь у вас есть больше строк кода для поддержки, поскольку теперь у вас есть дополнительная функция, которой не было раньше.)
  2. Подход, основанный на вызове членов функцией верхнего уровня, затрудняет использование класса, особенно программистами, которые также не являются разработчиками классов. Этот подход предоставляет открытый метод, который программисты не должны вызывать. Когда программист читает общедоступные методы класса, он видит два способа сделать то же самое. В документации должно быть сказано что-то вроде: «Это делает то же самое, но не используйте его; вместо этого используйте это». А средний программист скажет: «А? Зачем делать этот метод общедоступным, если я не должен его использовать?». На самом деле единственная причина, по которой метод printOn() является общедоступным, состоит в том, чтобы избежать присвоения статуса дружбы оператору operator<<, и это понятие находится где-то в области непонятного для программиста, который просто хочет использовать класс.

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

Примечание: если метод printOn() является защищенным (protected) или частным (private), второе возражение не применяется. Бывают случаи, когда такой подход разумен, например, при обеспечении печати для всей иерархии классов. Также обратите внимание, что когда метод printOn() не является общедоступным, operator<< должен быть другом (объявлен как friend).


Как я могу обеспечить ввод для моего класса Fred?

Используйте перегрузку операторов, чтобы предоставить дружественный оператор сдвига вправо, operator>>. Это похоже на оператор вывода, за исключением того, что параметр не имеет const: "Fred&", а не "const Fred&".

#include <iostream>
class Fred {
public:
  friend std::istream& operator>> (std::istream& i, Fred& fred);
  // ...
private:
  int i_;    // Просто для демонстрации
};

std::istream& operator>> (std::istream& i, Fred& fred)
{
  return i >> fred.i_;
}

int main()
{
  Fred f;
  std::cout << "Enter a Fred object: ";
  std::cin >> f;
  // ...
}

Обратите внимание, что operator>> возвращает поток. Это сделано для того, чтобы операции ввода можно было каскадировать и/или использовать в цикле while или if.


Как я могу обеспечить печать для всей иерархии классов?

Предоставьте дружественный operator<<, который вызывает защищенную виртуальную функцию:

class Base {
public:
  friend std::ostream& operator<< (std::ostream& o, const Base& b);
  // ...
protected:
  virtual void printOn(std::ostream& o) const = 0;  // Или просто virtual; смотрите ниже
};

inline std::ostream& operator<< (std::ostream& o, const Base& b)
{
  b.printOn(o);
  return o;
}

class Derived : public Base {
public:
  // ...
protected:
  virtual void printOn(std::ostream& o) const;
};

void Derived::printOn(std::ostream& o) const
{
  // ...
}

Конечным результатом является то, что operator<<действует так, как если бы он был динамически связан, даже если это дружественная функция. Это называется идиомой виртуальной дружественной функции.

Обратите внимание, что производные классы переопределяют printOn(std::ostream&) const. В частности, они не предоставляют собственного оператора operator<<.

Что касается того, является ли Base::printOn() простым виртуальным или чисто виртуальным методом, подумайте о том, чтобы сделать его простым виртуальным (без "= 0"), если можете реализовать эту функцию с кодом, который в противном случае был бы повторен в двух или более производных классах. Однако, если Base – это абстрактный базовый класс (Abstract Base Class, ABC) с небольшим количеством членов данных или вообще без них, возможно, вы не сможете дать осмысленное определение Base::printOn(), и вам следует сделать его чисто виртуальным. Если вы не уверены, сделайте его чисто виртуальным, по крайней мере, до тех пор, пока вы не научитесь лучше обращаться с производными классами.


Как открыть поток в двоичном режиме?

Используйте std::ios::binary.

Некоторые операционные системы различают текстовый и двоичный режимы. В текстовом режиме последовательности конца строки и, возможно, другие вещи преобразуются; в двоичном режиме – нет. Например, в текстовом режиме под Windows "\r\n" преобразуется в "\n" при вводе, а при выводе выполняется обратный перевод.

Чтобы прочитать файл в двоичном режиме, используйте что-то вроде этого:

#include <string>
#include <iostream>
#include <fstream>

void readBinaryFile(const std::string& filename)
{
  std::ifstream input(filename.c_str(), std::ios::in | std::ios::binary);
  char c;
  while (input.get(c)) {
    // ... делаем здесь что-то с переменной c ...
  }
}

Примечание: input >> c отбрасывает начальные пробелы, поскольку обычно вы не используете их при чтении двоичных файлов.


Как я могу «повторно открыть» std::cin и std::cout в двоичном режиме?

Это зависит от реализации. Обратитесь к документации вашего компилятора.

Например, предположим, что вы хотите выполнять двоичный ввод/вывод с помощью std::cin и std::cout.

К сожалению, не существует стандартного способа вызвать открытие std::cin, std::cout и/или std::cerr в двоичном режиме. Закрытие потоков и попытка повторно открыть их в двоичном режиме могут привести к неожиданным или нежелательным результатам.

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


Как я могу записывать/читать объекты моего класса в/из файла данных?

Прочтите раздел о сериализации объектов.


Как я могу отправить объекты моего класса на другой компьютер (например, через сокет, TCP/IP, FTP, электронную почту, беспроводную связь и т.д.)?

Прочтите раздел о сериализации объектов.


Почему я не могу открыть файл в другом каталоге, например "..\test.dat"?

Потому что "\t" – это символ табуляции.

В именах файлов вы должны использовать прямой слеш, даже в операционных системах, которые используют обратный слеш (DOS, Windows, OS/2 и т.д.). Например:

#include <iostream>
#include <fstream>

int main()
{
  #if 1
    std::ifstream file("../test.dat");  // ПРАВИЛЬНО!
  #else
    std::ifstream file("..\test.dat");  // НЕПРАВИЛЬНО!
  #endif
  // ...
}

Помните, что обратный слеш ("\") используется в строковых литералах для создания специальных символов: "\n" – это новая строка, "\b" – это обратный пробел, "\t" – это табуляция, "\a" – это «alert», "\v" – это вертикальная табуляция и т.д. Поэтому имя файла "\version\next\alpha\beta\test.dat" интерпретируется как набор очень забавных символов. На всякий случай используйте вместо него "/version/next/alpha/beta/test.dat", даже в системах, в которых в качестве разделителя каталогов используется "\". Это связано с тем, что библиотечные процедуры в этих операционных системах взаимозаменяемо обрабатывают символы "/" и "\".

Конечно, вы можете использовать "\\version\\next\\alpha\\beta\\test.dat", но это может навредить вам (есть ненулевой шанс, что вы забудете один из "\", a это довольно неуловимая ошибка, поскольку большинство людей ее не замечают), и это не может вам помочь (нет никакой пользы от использования "\\" вместо "/"). Кроме того, «/» более портабелен, поскольку он работает на всех разновидностях Unix, Plan 9, Inferno, всех Windows, OS/2 и т.д., а "\\" работает только с подмножеством из этого списка. Таким образом, "\\" вам чего-то стоит и ничего не приносит: используйте вместо него "/".


Как я могу узнать (была ли нажата клавиша, и это какая клавиша) до того, как пользователь нажмет клавишу ENTER?

Это не стандартная функция C++ – C++ даже не требует, чтобы в вашей системе была клавиатура! Это означает, что каждая операционная система и производитель делают это по-своему.

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

(Кстати, этот процесс в UNIX обычно состоит из двух шагов: сначала установите терминал в односимвольный режим, затем используйте select() или poll(), чтобы проверить, была ли нажата клавиша. Вы можете адаптировать этот код.)


Как сделать так, чтобы клавиши, нажимаемые пользователями, не отображались на экране?

Это не стандартная функция C++ – C++ даже не требует, чтобы в вашей системе была клавиатура! Это означает, что каждая операционная система и производитель делают это по-своему.

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


Как я могу перемещать курсор по экрану?

Это не стандартная функция C++ – C++ даже не требует, чтобы в вашей системе был экран! Это означает, что каждая операционная система и производитель делают это по-своему.

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


Как очистить экран? Есть что-нибудь вроде clrscr()?

Это не стандартная функция C++ – C++ даже не требует, чтобы в вашей системе был экран! Это означает, что каждая операционная система и производитель делают это по-своему.

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


Как я могу изменить цвета на экране?

Это не стандартная функция C++ – C++ даже не требует, чтобы в вашей системе был экран! Это означает, что каждая операционная система и производитель делают это по-своему.

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


Как я могу напечатать char как число? Как я могу напечатать char*, чтобы вывод отображал числовое значение указателя?

Преобразуйте его.

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

#include <iostream>
#include <string>

void f()
{
  char c = 'x';
  std::string s = "Now is";
  const char* t = "the time";
  std::cout << c;     // Печатает символ, в данном случае, x
  std::cout << 'y';   // Печатает символ, в данном случае, y
  std::cout << s[2];  // Печатает символ, в данном случае, w
  std::cout << t[2];  // Печатает символ, в данном случае, e
}

Потоки C++ при печати char* также поступают правильно: они выводят строку, которая должна заканчиваться символом '\0'.

#include <iostream>
#include <string>

void f()
{
  const char* s = "xyz";
  std::cout << s;     // Печатает строку, в данном случае, xyz
  std::cout << "pqr"; // Печатает строку, в данном случае, pqr
}

Эти вещи кажутся очевидными только потому, что они интуитивно понятны, но на самом деле в них реализованы довольно замечательные функции. Потоки C++ интерпретируют значения ваших символов в реальные, читаемые символы в соответствии с вашими текущими языковыми настройками, плюс они знают, что если вы даете им указатель на символ, вы, вероятно, захотите напечатать C-подобную строку. Единственная проблема – это когда вы не хотите, чтобы код вел себя подобным образом.

Представьте, что у вас есть структура, которая хранит возраст людей в виде unsigned char. Если бы вы хотели распечатать эту структуру, не имело бы большого смысла говорить, что у человека 'A'. Или, если по какой-то причине вы хотите напечатать адрес этой переменной возраста, поток начнется с этого адреса и будет интерпретировать каждый последующий байт (байты вашей структуры или класса, или даже стека!) как символ, остановившись, только когда он достигает первого байта, содержащего '\0'.

// Переменная age хранит возраст человека
unsigned char age = 65;

// Наша цель здесь - напечатать возраст человека:
std::cout << age;   // Упс! Печатается 'A', а не число возраста

// Наша следующая цель - напечатать расположение переменной age, то есть ее адрес:
std::cout << &age;  // Упс! Печатается мусор, и возможен сбой

Это не то, чего мы хотели. Самое простое и обычно рекомендуемое решение – привести char или char* к типу, который ваш компилятор не интерпретирует как символы, соответственно к int или void*:

// Переменная age хранит возраст человека
unsigned char age = 65;

// Наша цель здесь - напечатать возраст человека:
std::cout << static_cast<unsigned>(age);      // Хорошо: печатается 65

// Наша следующая цель - напечатать расположение переменной age, то есть ее адрес:
std::cout << static_cast<const void*>(&age);  // Хорошо: печатается адрес переменной

Это отлично подходит для явно указанных типов, таких как unsigned char, как показано выше. Но если вы создаете шаблон, в котором тип unsigned char из примера выше известен как просто некоторый числовой тип T, вы не хотите предполагать, что правильный числовой тип это unsigned или что-то еще. В этом случае вы хотите преобразовать свой объект T в правильный числовой тип, чем бы он ни был.

Например, ваш тип T может быть любым, от char до int до long или long long (если ваш компилятор поддерживает его). Или ваш тип T может даже быть абстрактным числовым классом, который даже не обеспечивает приведение к какому-либо встроенному целому числу (например, классы safe_integer, ranged_integer или big_num).

Один из способов справиться с этим – использовать трейты или специализацию шаблонов, но есть гораздо более простое решение, которое работает для типов char, не подвергая опасности эти другие типы. Пока тип T предоставляет унарный оператор + с обычной семантикой [*сноска], который предоставляется для всех встроенных числовых типов, всё будет работать нормально:

template <typename T>
void my_super_function(T x)
{
  // ...
  std::cout << +x << '\n';  // продвигает x к типу, печатаемому как число, независимо от типа
  // ...
}

Работает как по волшебству. Худшее, о чем вам нужно беспокоиться сейчас, это то, что это может показаться немного загадочным для других разработчиков. Если вы думаете про себя: «Вероятно, мне следует создать функцию под названием promote_to_printable_integer_type(), чтобы мой код самодокументировался». К сожалению, в C ++ в настоящее время отсутствует вывод типа, поэтому для написания такой функции потребуется код настолько сложный, что он, вероятно, принесет (потенциально) больше ошибок, чем вы могли бы предотвратить. В краткосрочной перспективе лучшее решение – просто стиснуть зубы и использовать operator+ и прокомментировать свой код.

Когда ваша организация получит доступ к C++ 11, вы сможете начать пользоваться удобством вывода типов:

template <typename T>
auto promote_to_printable_integer_type(T i) -> decltype(+i)
{
  return +i;
}

Не вдаваясь в подробности, возвращаемый тип – это «тот же тип, что и тип +i». Это может показаться странным, но, как и для большинства универсальных шаблонов, важна простота использования, а не красота определения самого шаблона. Ниже показан пример использования:

void f()
{
  unsigned char age = 65;
  std::cout << promote_to_printable_integer_type(age);  // Печатает 65
}
template <typename T>
void g(T x)
{
  // ...
  std::cout << promote_to_printable_integer_type(x);  // Работает для любого T, обеспечивающего унарный +
  // ...
}

Этот ответ будет обновлен в связи с выводом типа C++ 11. Следите за обновлениями в ближайшем будущем!!

[*сноска] Если вы определяете класс, представляющий число, чтобы предоставить унарный оператор + с канонической семантикой, создайте operator+(), который просто возвращает *this либо по значению, либо по ссылке на константу.

Теги

C++ / CppFAQiostreamoperator<<operator>>std::cinstd::coutВвод/выводВысокоуровневые языки программированияПрограммированиеЯзыки программирования

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

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