6
\$\begingroup\$

Today I am working on refactoring and documenting some ancient Delphi code. In that language, which is derived from Pascal, there are three forms of comments:

(* This is the original Pascal style comment, which is never used in my files *)
{ this is TurboPascal-style comment which I want to remove }
// This is a Delphi-style comment which  I want to remove 

As noted in this small sample, I want to strip out the last two styles of comments. The first kind of comment is never used in the code files I'm using, so there is no special processing done if they are encountered. I decided to do this using a std::streambuf. The code and some sample input is below. I'm interested in any comments for improvement.

commentfilterbuf.h

#ifndef COMMENTFILTER_H
#define COMMENTFILTER_H
#include <streambuf>

class commentfilterbuf : public std::streambuf {
    std::streambuf* sbuf;
    char buffer = EOF;
public:
    commentfilterbuf(std::streambuf* sbuf);
    int underflow(); 
};
#endif // COMMENTFILTER_H

commentfilterbuf.cpp

#include "commentfilterbuf.h"

commentfilterbuf::commentfilterbuf(std::streambuf* sbuf)
    : sbuf(sbuf)
{
}

int commentfilterbuf::underflow() {
    traits_type::int_type i;
    // skip comments which are delimited with { and }
    for (i = sbuf->sbumpc(); i == '{'; i = sbuf->sbumpc()) {
        while ((i = sbuf->sbumpc()) != '}');
    }
    if (traits_type::not_eof(i)) {
        // check for single line comments like this one
        if (i == sbuf->sgetc() && i == '/') {
            while ((i = sbuf->sbumpc()) != '\n');
        }
        buffer = traits_type::to_char_type(i);
        setg(&buffer, &buffer, &buffer+1);
    }
    return i;
}

testfilter.cpp

#include "commentfilterbuf.h"
#include <iostream>
#include <fstream>
#include <string>

int main(int argc, char* argv[])
{
    if (argc != 2) {
        std::cerr << "Usage: testfilter inputfile\n";
        return 0;
    }
    std::ifstream fin{argv[1]};
    if (!fin) {
        std::cerr << "Error: could not open " << argv[1] << "\n";
        return 1;
    }
    commentfilterbuf sbuf(fin.rdbuf());
    std::istream in(&sbuf);
    std::string line;
    for (std::string line; std::getline(in, line); ) {
        std::cout << line << '\n';
    }
}

sample.in

program HelloWorld;

{$APPTYPE CONSOLE}
{ This is a comment. }
{
    this is a multiline comment.
}
{  This is a Turbo Pascal comment }  
// This is a Delphi comment. All is ignored till the end of the line.
// let's try to confuse the parser
{ comment 1 // Comment 2 }begin
// comment 1 { comment 2 }foo

  // Here's a comment
  WriteLn('Hello World');  { and another }
end.
\$\endgroup\$
2
\$\begingroup\$

I only have a few minor comments.

The first is about your empty while loops, such as:

    for (i = sbuf->sbumpc(); i == '{'; i = sbuf->sbumpc()) {
        while ((i = sbuf->sbumpc()) != '}');
    }

I generally prefer to at least put the semicolon on a line by itself, to make it fairly obvious that it's intentional that the semicolon wasn't just accidentally entered at the end of a line where it shouldn't have been:

for (i = sbuf->sbumpc(); i == '{'; i = sbuf->sbumpc()) {
    while ((i = sbuf->sbumpc()) != '}')
        ;
}

In this case, there's no other statement there before the closing brace that the loop could be controlling so it's probably less serious than usual, but I still prefer consistency (even though: "a foolish consistency is the hobgoblin of small minds").

Second, if you're going to do all this, I'd probably add a little commentfilterstream class on this order:

#include <fstream>
#include <istream>

class commentfilterstream : public std::istream {
    std::filebuf buffer;
    commentfilterbuf buf;

public:
    // or use an `std::filesystem::path` instead of a string.
    commentfilterstream(std::string const& filename)
        : buf(&buffer)
        , std::istream(&buf)
    {
        buffer.open(filename.c_str(), std::ios_base::in);
    }

    ~commentfilterstream() { buffer.close(); }
};

This lets you simplify the client code a bit to become something like this:

int main(int argc, char* argv[])
{
    if (argc != 2) {
        std::cerr << "Usage: testfilter inputfile\n";
        return 0;
    }

    // Opening stream is now one step, just like usual:
    commentfilterstream in{argv[1]};

    if (!fin) {
        std::cerr << "Error: could not open " << argv[1] << "\n";
        return 1;
    }

    std::string line;
    for (std::string line; std::getline(in, line); ) {
        std::cout << line << '\n';
    }
}

Finally, I'd probably put the buffer and filter into a namespace (e.g., Delphi) to give a little better indication of their intended purpose (Delphi::commentfilterstream).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.