tableprinter - Table-like output

I have written a small library with the help of iomanip and ostream header which allows a programmer to print table-like data. I am using this table-like formatting on my daily job. For suitable scenarios, this formatting generates pretty readable text outputs.

I am sharing the repo, maybe someone attracts.

https://github.com/OzanCansel/tableprinter
Interesting implementation ...
I can't think of a case where integrating this library would be the best course of action. Mostly that's because the task is trivial, so it doesn't make sense to use a library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <iomanip>

template <typename T0, typename T1, typename T2, typename T3, typename T4>
  void print_row(std::ostream& os, T0 const& t0, T1 const& t1, T2 const& t2,
                 T3 const& t3, T4 const& t4)
  {
    os << std::setw(4) << t0 << std::setw(10) << t1 << std::setw(10) << t2
       << std::setw(6) << t3 << std::setw(7) << std::fixed << std::setprecision(2)
       << t4 << '\n';
  }

int main()
{
  print_row(std::cout, "id", "name"   , "surname"  , "rank", "score");
  print_row(std::cout, 1   , "Lucy"   , "Ballmer"  , 2     , 94.13);
  print_row(std::cout, 2   , "Roger"  , "Bacon"    , 5     , 77.13);
  print_row(std::cout, 3   , "Anna"   , "Smith"    , 3     , 87.13);
  print_row(std::cout, 4   , "Robert" , "Schwartz" , 1     , 98.34);
  print_row(std::cout, 5   , "Robert" , "Brown"    , 4     , 84.34);
}


I did quickly read the entire program, so I hope the following is of some concrete use. My most major criticism relates to how the library retains a collection of streams. The interface is more error-prone and harder to use as a result.

The underlying sequence of streams is a vector, but write-access to it isn't readily available. Instead, users are only given printer::remove_streams and printer::add_streams, a poor substitute for vector's suite of member functions and the contents of the standard library. The limited interface makes it much harder to manipulate the list of streams stored in the printer. Further, the need to store pointers (i.e., wrapped references) to those streams introduces potential problems involving dangling references.

Indeed there is no reason for the library to retain a sequence of streams at all, so this functionality should be removed.

I also found a few smaller issues, briefly:
1. One shouldn't throw standard exceptions directly, instead, derive types from them and throw objects of the derived type. This allows users to distinguish exceptions thrown by tableprinter from those coming from elsewhere.
2. filter_opts collects a vector of all the options whose type is convertible to const Opt&, but at every call site only the length of this vector is used.
3. Attempting to move elements out of an initializer_list<T> is futile because its element type is always const-qualified.
4. printer has a converting constructor from vector<column>; is this intentional?
5. The static assertion in printer::add_streams should use is_convertible and not is_base_of.
6. printer::remove_streams (at least) assumes the absence of operator& on a generic type; use std::addressof instead.
7. The library consistently passes function objects by reference, albeit not in a public interface. Whether this was intentional or not, it's inconsistent with every component in the standard library and may lead to surprising behavior. If function objects are passed by value, programmers who desire reference semantics can still opt-in by passing std::ref(my_function_object) as an argument.
Last edited on
@mbozzi very thanks to your post.

Firstly I must admit that, nowadays, I implement tiny libraries to improve my C++ and programming skills. So I see these implementations as attempts. Sometimes, I find myself in a sitation where I overengineered about the topic or implemented inefficient/wrong/bad way. Nevertheless, I believe that those concrete attempts are required for improvement. So if tableprinter is completely useless I would not be very sad. That's how I approach to the matter.

I will try to make some explanations.

Actually, I have thought about whether to print table in a class or a function. I decided not to do in a function because I want to introduce table notion which consists of rows and columns. So I decided that implementing in a class is a better way to express this notion. Btw, If i had decided to implement as a function I would not implement this library.

Also I assumed that there might be a situtation which another class want to print a row without knowing where it prints. This is the reasoning of why it retains m_streams with it.

An example :

1
2
3
4
5
6
7
8
9
10
11
struct analyser
{
    tableprinter::printer& m_printer;
    analyser( tableprinter::printer& p ) : m_printer { p }
    {}

    void do_some()
    {
        m_printer.print( 1 , 2 , 3 );
    }
};


The table notion which courages the user to think with it. So it is different than implementing a function which incidentally outputs like a table. Functionality might exactly be the same but the printer courages the user to think over the table notion.

I restrict the user to access m_streams because I don't want to access it freely. Either add or remove streams, nothing else is needed according to my perspective.

Constructor of printer tells that it needs references to ostreams which means they cannot be null. Dangling reference could happen but user already knows that the library expects non-null. Even though, I agree that it introduces some potential problems, it is up to him.

Answers of numbered issues :

1. - In my other library which is https://github.com/OzanCansel/fsconfig/blob/master/include/fsconfig/fsconfig.hpp#L12-L70, I implemented exceptions as you have said but in tableprinter I wanted to try to express over built-in stl exception classes but I agree that specialized exception classes are better, I will refactor this part.

2. - In this line https://github.com/OzanCansel/tableprinter/blob/master/src/tableprinter/tableprinter.hpp#L527 the elements of vector is used to concat column names.

3. - I didn't know that, functionality is same but it is better not to move. I will refactor this.

4. - I realized before last release but forgotten again. I will refactor this.

5. - Same question arose but I decided to use is_base_of but now I see is_convertible expresses intent better. I will refactor this.

6. - Nice catch, I wasn't aware of that. I will refactor this.

7. - Do you mean should F be taken as F rather than F&& at this line ? https://github.com/OzanCansel/tableprinter/blob/master/src/tableprinter/tableprinter.hpp#L137

Again very thanks for your detailed examination while you think that it is not a useful library.
Last edited on
Do you mean should F be taken as F rather than F&& at this line

This doesn't make a substantial difference to the quality of programs using your library, because what you have works fine and will work fine after the change.

A guideline could be "avoid accepting function objects by reference in interfaces". I have caused far too many runtime errors by violating this guideline, so now I try to mention it when I have an excuse.

The library was very easy to read and understand. I mostly read the code top to bottom and was able to understand most of it, which means it is well-organized. 600 lines is a lot of code to be so clear.

Also I think a sanity_check function is a great idea and wish it was more common.
Last edited on
Thank you :)

I refactored function object passing part and made value-by.
Topic archived. No new replies allowed.