Что такое на самом деле endl (или любой манипулятор вывода)? Как это реализовано и как работает с оператором <<? - PullRequest
0 голосов
/ 18 июня 2020

Что на самом деле endl? Конечно, он печатает новую строку и очищает буфер Ostream. Но что это на самом деле? Может ли программист определить такие «сущности», как endl?

Есть эти «манипуляторы вывода», к которым можно получить доступ с помощью библиотеки iomanip, но то, что на самом деле происходит при выполнении такой команды, как: cout << setprecision(5);

setprecision() выглядит так: вызов функции, но при использовании экземпляра cout ничего не печатается. Это изменяет точность, но почему бы просто не использовать соответствующий член функции вместо добавления дополнительной «абстракции» к написанию кода? Под абстракцией я подразумеваю неинтуитивный код.

Спасибо!

Ответы [ 2 ]

4 голосов
/ 18 июня 2020

Что такое на самом деле endl? Конечно, он печатает новую строку и очищает буфер Ostream. Но что это на самом деле?

std::endl - это функция. Он

Вставляет символ новой строки в выходную последовательность os и сбрасывает его


Могут ли такие "сущности", как endl, быть определены программистом?

Да.

Вот демонстрационная программа.

#include <iostream>

std::ostream& test_manip(std::ostream& out)
{
   return (out << "In test_manip\n");
}

int main()
{
   std::cout << test_manip;
}

и ее результат.

In test_manip
1 голос
/ 19 июня 2020

Что на самом деле endl? Конечно, он печатает новую строку и очищает буфер Ostream. Но что это на самом деле?

std::endl - это функция, которая принимает ссылку std::ostream& в качестве ввода и возвращает ссылку std::ostream& в качестве вывода:

template< class CharT, class Traits >
std::basic_ostream<CharT, Traits>& endl( std::basic_ostream<CharT, Traits>& os );

std::basic_ostream::operator<< имеет перегрузку, которая принимает указатель на такую ​​функцию:

template<
    class CharT,
    class Traits = std::char_traits<CharT>
> class basic_ostream : virtual public std::basic_ios<CharT,Traits>
{
    ...
    basic_ostream& operator<<(
        std::basic_ostream<CharT,Traits>& (*func)(std::basic_ostream<CharT,Traits>&)
    );
    ...
};

Эта перегрузка вызовет переданную функцию, присвоив ей std::ostream объект, для которого вызывается оператор, например:

template<class CharT, class Traits>
basic_ostream<CharT,Traits>& basic_ostream<CharT,Traits>::operator<<(
    std::basic_ostream<CharT,Traits>& (*func)(std::basic_ostream<CharT,Traits>&) )
{
    func(*this);
    return *this;
}

Реализация std::endl может затем записать в данный std::ostream и flu sh его, например:

template<class CharT, class Traits>
std::basic_ostream<CharT,Traits>& endl( std::basic_ostream<CharT,Traits>& os )
{
    os.put(os.widen('\n'));
    os.flush();
    return os;
}   

Итак, когда у вас есть такой оператор:

std::cout << std::endl

Он будет вызывать это внутренне:

std::cout.operator<<(&std::endl)

Что затем вызовет:

std::endl(std::cout)

Может ли программист определить такие "объекты", как endl?

Да. Любая функция, которая соответствует указанной выше сигнатуре (std::basic_ostream<CharT,Traits>& (*)(std::basic_ostream<CharT,Traits>&)), может быть передана в operator<<.

, что на самом деле происходит при выполнении такой команды, как: cout << setprecision(5);

setprecision() выглядит как вызов функции

Это IS вызов функции. Манипуляторы ввода-вывода, которые принимают вводимые пользователем данные, работают немного иначе, чем манипуляторы ввода-вывода, которые не принимают никаких вводимых пользователем данных. object), такой манипулятор возвращает экземпляр определенного реализацией типа, который содержит ввод, а затем перегружает не-член operator<<, чтобы принять этот тип. Когда эта перегрузка вызывается, она может затем применить ввод к std::ostream (или std::istream) по мере необходимости.

В случае std::setprecision() он принимает int в качестве ввода, и возвращает определяемый реализацией тип, содержащий этот int, а затем этот тип передает int в std::ostream::precision(), например:

struct PrecisionType { int value; };

PrecisionType setprecision( int n )
{
    return PrecisionType{ n };
}

template<class CharT, class Traits>
std::basic_ostream<CharT,Traits>& operator<<( std::basic_ostream<CharT,Traits>& os, const PrecisionType &input )
{
    os.precision(input.value);
    return os;
}

Таким образом, такой оператор:

std::cout << std::setprecision(5)

На самом деле вызовет что-то вроде этого:

PrecisionType temp = std::setprecision(5);
operator<<(std::cout, temp)

Что затем вызовет внутренне:

std::cout.precision(temp.value)

, но при использовании экземпляра cout ничего не печатается. Он изменяет точность

Правильно, потому что манипулятор, который возвращает set::setprecision(), ничего не записывает в выходной буфер std::ostream, он просто настраивает сам std::ostream.

Ничто не мешает манипулятору ввода-вывода писать в std::ostream (или читать из std::istream), если он этого хочет.

почему бы просто не использовать соответствующую функцию элемент вместо добавления дополнительной «абстракции» к написанию кода?

Конечно, вы могли бы вызывать элементы напрямую, но тогда вы не смогли бы связать последующие выражения, используя <<. То, что каждый манипулятор возвращает ссылку на управляемый объект std::ostream (или std::istream), - вот что позволяет связывание. Методы-члены не возвращают такие ссылки.

Например:

cout << setprecision(5) << 123.45 << endl;

Преобразуется в это:

operator<<(cout, setprecision(5)).operator<<(123.45).operator<<(&endl);

Что в конечном итоге вызовет что-то вроде этого внутри:

//operator<<(cout, setprecision(5));
cout.precision(5);

//cout.operator<<(123.45);
use_facet<num_put<char>>(cout.getloc()).put(
  ostreambuf_iterator it{cout},
  cout,
  cout.fill(),
  123.45
);

//cout.operator<<(&endl)
endl(cout);

Не так красиво, как просто использовать << перегрузки, не так ли?

...