License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Hervé Frezza-Buet, CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : streams.md

Table of contents

Operator overloading

Function versus method

struct X {...};
X operator+(const X& arg1, const X& arg2) {...; return res;}

X a,b,c;
c = a + b; // c = operator+(a, b);
struct X {
X operator+(const X& arg2) const {const X& arg1 = *this; ...; return res;}
}

X a,b,c;
c = a + b; // c = a.operator+(b);

Which operators can be overloaded ?

Almost all which already exist in c++ (see here).

Nota, since C++20 :

// From en.cppreference.com/w/cpp
struct Record
{
    std::string name;
    unsigned int floor;
    double weight;
    auto operator<=>(const Record&) const = default;
};
// records can now be compared with ==, !=, <, <=, >, and >=

Operators new and delete can be overloaded.

Use them when the clarify the syntax. For example, in the Eigen library:

Matrix3f m;
m << 1, 2, 3,
     4, 5, 6,
     7, 8, 9;

A home made example

Let us provide a library for defining successive postures of a robotic arm. We would like to have this code:

#include "arm.hpp"
#include <iostream>
#include <iomanip>

int main(int argc, char* argv[]) {
  arm::Device manipulator;
  int s1,e1,w1;
  int s2,e2,w2;

  manipulator <= arm::go,
                 arm::wrist(10), arm::go,
                 arm::shoulder(45), arm::elbow(120), arm::go,
                 arm::read(s1,e1,w1),
                 arm::wrist(10), arm::elbow(90), arm::go,
                 arm::elbow(0), arm::go,
                 arm::read(s2,e2,w2);
  
  std::cout << std::endl
	    << "Readings : " << std::endl
	    << "  " << std::setw(3) << s1 << ", "
	    << "  " << std::setw(3) << e1 << ", "
	    << "  " << std::setw(3) << w1 << std::endl
	    << "  " << std::setw(3) << s2 << ", "
	    << "  " << std::setw(3) << e2 << ", "
	    << "  " << std::setw(3) << w2 << std::endl;
  
  return 0;
}

See the file arm.hpp for the implementation.

Streams

Make your class stream complient.

#include <iostream>

class X;
std::ostream& operator<<(std::ostream&, const X&);
std::istream& operator>>(std::istream&,       X&);

class X {
private:
  friend std::ostream& operator<<(std::ostream&, const X&);
  friend std::istream& operator>>(std::istream&,       X&);
};

std::ostream& operator<<(std::ostream& os, const X& x) {os << ... ; return os;}
std::istream& operator>>(std::istream& is,       X& x) {is >> ... ; return is;}

...

X a, b, c;
std::cin >> a >> b >> c;
std::cout << a << ' ' << b << ", " << c << std::endl;

Output streams manipulators

#include <iostream>
#include <iomanip>
#include <sstream>
#include <fstream>

unsigned int choice;
std::cout << "Enter a number: " << std::flush; // Flush forces the display without a new line.
std::cin >> choice;

std::ostringstream file_name;
file_name << "Toto-"
          << std::setw(6) << std::setfill('0') << choice // writes 000003 for choice == 3.
          << ".data";
{
  std::ofstream file(file_name.str());                   // Opens "Toto-000003.data" for choice == 3.
  if(file) file << ....
} // File is closed when it is destructed.

Input streams manipulators

#include <iostream>
#include <sstream>
#include <string>
#include <stdexcept>

#include <num.hpp>

/* The **predictive** grammar is for nums is :
   NUM        := <int> | STRING_NUM
   STRING_NUM := STRING STRING_END
   STRING     := ' <charlist> '       
   STRING_END := <empty> | = <int> */

// Parsing functions.
num         NUM       (std::istream&);
num         STRING_NUM(std::istream&);
std::string STRING    (std::istream&);

int main(int argc, char* argv[]) {
  std::string nums = "3  'a'=4    'b is my name' =    -832  'no value' 578  'c'= 4 314 ";
  std::istringstream ifs(nums); 
  ifs.exceptions(std::ios::failbit | std::ios::badbit | std::ios::eofbit); // We set exception handling.
  num x {"x", 0};
  ___;
  try{
    while(true) {
      x = NUM(ifs);
      ___;
    }
  }
  catch(const std::exception& e) {/* Nothing to be done here */}

  return 0;
}

num NUM(std::istream& is) {
  char c;
  is >> std::ws // Eats white spaces (tab, space, return...). Not mandatory here.
     >> c;
  // We have peeked the next non white char, we put it back in the stream.
  // We can choose what to do next according to c... the grammar is predictive.
  is.putback(c);
  switch(c) {
  case '\'' :                     return STRING_NUM(is); break;
  default   : int val; is >> val; return num(val);       break;
  }
}

num STRING_NUM(std::istream& is) {
  char c;
  std::string name = STRING(is);
  is >> c; // We check is = is there.
  if(c == '=') {
    int val; is >> val;
    return num(name, val);
  }
  is.putback(c); 
  return num(name);
}
  
std::string STRING(std::istream& is) {
  char c;
  std::string result;
  is >> c; // We eat first '
  std::getline(is, result, '\'');
  return result;
}

Stream iterators

#include <iostream>
#include <sstream>
#include <fstream>
#include <algorithm>
#include <vector>
#include <iterator>

struct point {double x, y;};
std::ostream& operator<<(std::ostream& os, const point& p) {          os << '(' << p.x << ", " << p.y << ')'; return os;}
std::istream& operator>>(std::istream& is,       point& p) {char sep; is >> sep >> p.x >> sep  >> p.y >> sep; return is;}

int main(int argc, char* argv[]) {
  std::istringstream is("(1.2, 3.4)(5,6.78)   (9.012, 3) (4,5.67)");
  std::vector<point> points;
  // We read the file into a vector.
  std::copy(std::istream_iterator<point>(is), std::istream_iterator<point>(), std::back_inserter(points));   
  // We dump the vector to a file.
  std::ofstream os("points.txt");
  if(os) std::copy(points.begin(), points.end(), std::ostream_iterator<point>(os));
  return 0;
}

Binary streams

The number 123.4567890 is written with 11 characters, i.e. 11 ASCII-encoded bytes, while the double representation in memory is sizeof(double) = 8. So passing ASCII representations in streams makes the file human readable (as in .csv files), but is is not efficient, in terms of file-size as wall as in terms of computing the binary representation from the decimal ASCII syntax.

This is why, sometimes, binary representations are more efficient.

With binary files, you can handle a reading and a writing head, and call read or write to get or put bytes where the heads are pointing. See methods like tellp, tellg, seekp, seekg.

Here is an example of an emitter written in python that sends a byte stream to a receiver written in C++.

# file emitter.py
import numpy as np
import sys

bunch = np.arange(30).reshape(10,3).astype(np.float) # [[0, 1, 2], [3, 4, 5], ..., [27, 28, 29]]
for i in range(200):
    sys.stdout.buffer.write(b'\x01') # means "Data is comming next".
    sys.stdout.buffer.write(bunch.tobytes())
    bunch += 1000 # fake change of the data points
    
sys.stdout.buffer.write(b'\x00') # means "I am done".
sys.stdout.flush()
// file receiver.cpp
#include <iostream>
#include <array>

struct datum {double x,y,z;};
using buffer = std::array<datum, 10>;

std::ostream& operator<<(std::ostream& os, const datum&  d) {os << '(' << d.x << ", " << d.y << ", " << d.z << ')'; return os;}
std::ostream& operator<<(std::ostream& os, const buffer& d) {
  auto it = d.begin();
  os << '[' << *(it++);
  while(it != d.end()) os << ", " << *(it++);
  os << ']';
  return os;
}

int main(int argc, char* argv[]) {
  buffer buf;          // sizeof(buffer) bytes = 10*sizeof(datum) bytes = 10*3*sizeof(double) bytes = 10*3*8 bytes
  for(char has_next_tag = std::cin.get(); has_next_tag; has_next_tag = std::cin.get()) {
    std::cin.read(reinterpret_cast<char*>(std::data(buf)), sizeof(buffer));
    std::cout << buf << std::endl;
  } 
  return 0;
}
mylogin@mymachine:~$ g++ -o reveiver -std=c++17 receiver.cpp 
mylogin@mymachine:~$ python3 emitter.py | ./reveiver
[(0, 1, 2), (3, 4, 5), ..., (27, 28, 29)]
[(1000, 1001, 1002), (1003, 1004, 1005), ..., (1027, 1028, 1029)]
[(2000, 2001, 2002), (2003, 2004, 2005), ..., (2027, 2028, 2029)]
...
Hervé Frezza-Buet,