Как работают манипуляторы потока?
Хорошо известно, что пользователь может определять манипуляторы потоков следующим образом:
ostream& tab(ostream & output)
{
return output<< '\t';
}
И это можно использовать в main() следующим образом:
cout<<'a'<<tab<<'b'<<'c'<<endl;
Пожалуйста, объясните мне, как это все работает? Если оператор < принимает в качестве второго параметра указатель на функцию, которая принимает и возвращает ostream &, то, пожалуйста, объясните, почему это необходимо? Что было бы неправильно, если функция не принимает и возвращает ostream &, но она была недействительной вместо ostream &?
Также интересно, почему манипуляторы "dec", "hex" вступают в силу до тех пор, пока я не изменюсь между ними, но пользовательские манипуляторы должны всегда использоваться для вступления в силу для каждой потоковой передачи?
Ответы
Ответ 1
Стандарт определяет следующую перегрузку operator<<
в шаблоне класса basic_ostream
:
basic_ostream<charT,traits>& operator<<(
basic_ostream<charT,traits>& (*pf) (basic_ostream<charT,traits>&) );
Эффекты: Нет. Не ведет себя как форматированная выходная функция (как описано в 27.6.2.5.1).
Возвращает: pf(*this)
.
Параметр - это указатель на функцию, принимающую и возвращающую ссылку на std::ostream
.
Это означает, что вы можете "передать" функцию с этой сигнатурой в объект ostream
, и она имеет эффект вызова этой функции в потоке. Если вы используете имя функции в выражении, то оно (обычно) преобразуется в указатель на эту функцию.
std::hex
является манипулятором std::ios_base
, определенным следующим образом.
ios_base& hex(ios_base& str);
Эффекты: Вызовы str.setf(ios_base::hex, ios_base::basefield)
.
Возвращает: str.
Это означает, что потоковая передача hex
на ostream
будет устанавливать флаги форматирования выходной базы для вывода чисел в шестнадцатеричном формате. Манипулятор ничего не выводит.
Ответ 2
Нет ничего плохого в этом, за исключением того, что нет перегруженного < < оператор, определенный для него. Существующие перегрузки для < ожидают манипулятора с сигнатурой ostream & (* FP) (ostream &).
Если вы дали ему манипулятор с типом ostream & (* fp)() вы получили бы ошибку компилятора, так как не имеет определение для оператора < < (ostream &, ostream & (* fp)() ). Если вы хотите эту функциональность, вам придется перегрузить < оператора для приема манипуляторов этого типа.
Вам нужно написать определение для этого:
ostream & ostream:: operator < (ostream & (* m)())
Имейте в виду, что здесь ничего магического не происходит. Библиотеки потоков в значительной степени зависят от стандартных возможностей С++: перегрузка операторов, классы и ссылки.
Теперь, когда вы знаете, как вы можете создать описанную вами функциональность, вот почему мы этого не делаем:
Не передавая ссылку на поток, который мы пытаемся манипулировать, мы не можем вносить изменения в поток, подключенный к окончательному устройству (cin, out, err, fstream и т.д.). Функция (модификатор - все просто функции с причудливыми именами) либо должна была бы вернуть новый поток, который не имел ничего общего с тем, что слева от символа < оператора или через какой-то очень уродливый механизм, выясните, какой ostream он должен подключиться, иначе все, что направо от модификатора, не будет доведено до конечного устройства, но скорее будет отправлено в любой поток, возвращаемый функцией/модификатором.
Подумайте о таких потоках
cout << "something here" << tab << "something else"<< endl;
действительно означает
(((cout << "something here") << tab ) << "something else" ) << endl);
где каждый набор круглых скобок делает что-то cout (запись, изменение и т.д.), а затем возвращает cout, поэтому следующий набор круглых скобок может работать на нем.
Если ваш модификатор/функция вкладки не принимал ссылку на ostream, ему нужно было бы как-то угадать, какой ostream был слева от < оператора для выполнения своей задачи. Вы работали с курсом, cerr, некоторым файловым потоком...? Внутренности функции никогда не узнают, если они не передадут эту информацию каким-то образом, и почему бы не так, как это было бы просто, как ссылка на нее.
Теперь, чтобы действительно поместить точку дома, давайте посмотрим, что действительно есть endl и какая перегруженная версия < мы используем:
Этот оператор выглядит следующим образом:
ostream& ostream::operator<<(ostream& (*m)(ostream&))
{
return (*m)(*this);
}
endl выглядит следующим образом:
ostream& endl(ostream& os)
{
os << '\n';
os.flush();
return os;
}
Цель endl - добавить новую строку и очистить поток, убедившись, что все содержимое внутреннего буфера потока записано на устройство. Для этого сначала нужно написать '\n' этому потоку. Затем ему нужно сообщить потоку, чтобы он промолчал. Единственный способ, с помощью которого endl узнать, какой поток писать и скрывать, - это передать оператор этой функции функции endl при ее вызове. Было бы так, как будто я говорю, что ты моешь свою машину, но никогда не говоришь, какая машина моя на всей стоянке. Вы никогда не сможете выполнить свою работу. Мне нужно, чтобы я либо передал тебе свою машину, либо сам помылся.
Я надеюсь, что это очистит вещи
PS - Если вы случайно случайно нашли мой автомобиль, пожалуйста, вымойте его.
Ответ 3
Обычно манипулятор потока устанавливает некоторые флаги (или другие настройки) для объекта потока, так что в следующий раз, когда он будет использоваться, он будет действовать в соответствии с флагами. Поэтому манипулятор возвращает тот же объект, который был передан. Перегрузка operator<<
, которая называется манипулятором, уже имеет этот объект, конечно, так, как вы заметили, возвращаемое значение строго не требуется для этого случая. Я думаю, что это охватывает все стандартные манипуляторы - все они возвращают свой вклад.
Однако с возвращаемым значением структура достаточно гибкая, чтобы пользовательский манипулятор потока мог возвращать другой объект, предположительно, оболочку для объекта, который он задал. Этот другой объект затем будет возвращен из cout << 'a' << tab
и может сделать то, что не поддерживает встроенные параметры форматирования ostream
.
Не уверен, как бы вы решили освободить этот другой объект, поэтому я не знаю, насколько это практично. Возможно, это должно быть что-то своеобразное, как прокси-объект, которым управляет сам ostream
. Тогда манипулятор будет работать только для пользовательских классов потоков, которые активно поддерживают его, что обычно не является точкой манипуляторов.