Archives For C++

fun with c++ 14

May 8, 2016

c++mugI am an Old Geek and this blog is titled Arcane Science Lab for a reason. It should come as no surprise then when I write about very Geeky Things like languages, especially languages like C and C++. While there are plenty of young turks running around espousing the virtues of languages such as Javascript, and their implementations in Node.js and frameworks such as Angular 2, React, and jQuery, the old guard languages such as C and C++ still hold considerable relevance. It needs to be noted that all those other languages are written in C and C++, especially Node.js, which is built on top of Google’s V8 engine, the Javascript engine that runs inside of Chrome. And of course, the Linux kernel, for better or worse, is written in C.

With all that in mind I’ve been looking to update my rusty C and C++ skills. I don’t write C/C++ code commercially anymore because the opportunities in Orlando are based around fairly ancient versions of the compiler. While I want to work with C++ 11 and C++ 14 as well as C11, too many projects are using RHEL 6 or earlier, or its equivalent CentOS 6 or earlier. RHEL 6 ships with gcc 4.4.7, which has no support for contemporary C or C++.

In order to play in those latest C and C++ versions I used Homebrew on my MBP to install gcc 5.x and gcc 6. I then use a Makefile to set up my build environment to use a specific version, thusly;

CC = g++-6
LDFLAGS = -L/usr/local/opt/isl014/lib
CPPFLAGS = -I/usr/local/opt/isl014/include

TARGET = generic_sort

all: $(TARGET)

clean:
    rm $(TARGET)

$(TARGET): $(TARGET).cpp
    $(CC) $(LDFLAGS) $(CPPFLAGS) -o $(TARGET) $(TARGET).cpp

This is the simplest makefile I can come up with. A single TARGET is defined and can be redefined for any simple project. The version of the compiler to use is defined at the top of the makefile along with any special flags (which was displayed as a caveat when I ran ‘brew install homebrew/versions/gcc6’). The test c++ file I used to test the installation is;

#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>

int main() {
    std::vector<int> V(10);

    // Use std::iota to create a sequence of integers 0, 1, ...
    std::iota(V.begin(), V.end(), 1);

    // Print the unsorted data using std::for_each and a lambda
    std::cout << "Original data" << std::endl;
    std::for_each(V.begin(), V.end(), [](auto i) { std::cout << i << " "; });
    std::cout << std::endl;

    // Sort the data using std::sort and a lambda 
    std::sort(V.begin(), V.end(), [](auto i, auto j) { return (i > j); });

    // Print the sorted data using std::for_each and a lambda
    std::cout << "Sorted data" << std::endl;
    std::for_each(V.begin(), V.end(), [](auto i) { std::cout << i << " "; });
    std::cout << std::endl;

    return 0;
}

This small application won’t compile with the latest Apple Xcode C++, but it will compile with Homebrew’s installed version.

I’m looking at C and C++ due to the Internet of Things (and small computers such as the Raspberry Pi and the BeagleBone Black rev C) and the limited resources found on such devices. I’m also interested because that’s my background, going back to when I started with Lifeboat C on an IBM PC XT in 1983. I might meander over to Java or Python or Javascript, but sooner or later, usually sooner, C and C++ call me back, especially when I want to go down to the bare silicon.

More to come…

There comes a time in any programmer’s life when they write logging functions. Sometimes it’s little more than std::cout <<, or sometimes it’s a full bore implementation along the lines of Boost.Log. Sometimes you want more than a print statement without the overhead of a Boost.Log implementation. What I’m about to list is that kind of logging function. It comes in just two files, a header and implementation, with a simple test file to try it out.

First, the code listings.

#ifndef IT_LOGGER_H
#define IT_LOGGER_H

#include <iostream>

namespace IT
{
    class Logger {
        public:
        enum Level {
            INFO,   // Everything.
            ERROR,  // An error in the code itself.
            FATAL   // What is caused by a runtime exception.
        };

        // Set Logger to an output stream. Default is std::cout.
        //
        explicit Logger(std::ostream &ostr);

        // Set the stream we want to log to. Default is std::cout.
        //
        static std::ostream &setStream(std::ostream *ostr = 0);

        // Set the reporting level. Default is INFO.
        //
        static void setLevel(const IT::Logger::Level = INFO);

        // The logging function. This is wrapped in the LOG macro below.
        //
        static void log(const Level, const std::string fileName, const int line, const std::string msg);
    };
}

// Generate a logger entry. Macros are evil, but this is a necessary evil.
//
#define LOG(level, string) \
    IT::Logger::log((IT::Logger::level), __FILE__, __LINE__, (string));

#endif

it_logger.h

#include <cctype>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <sstream>
#include <strings.h>

#include "it_logger.h"

// All the code in this anonymous namespace block is executed once and only once
// during application startup. This is a "silent" new for Logging to make sure
// it is properly configured and sane.
//
namespace
{
    // Check to see where logging output will go. Default is standard out.
    //
    // If the user defines the environmental variable ITLOGFILE, then that
    // will be the logging output. The file will be opened relative to the
    // home directory of the user running the application. For example, if
    // the user, using bash, performs 'export ITLOGFILE=foo.txt', then runs
    // the application, all logging will go to $HOME/foo.txt.
    //
    // Failures to open are silent, and the default on failure is standard out.
    //
    std::ostream* checkLoggingOutput(void) {
        std::ostream *logout = &std::cout;
        std::stringstream logfilename;
        char *home = getenv("HOME");
        char *env = getenv("ITLOGFILE");

        if ((home != NULL) && (env != NULL)) {
            logfilename << home << "/" << env;
            std::ofstream *ofs =
                new std::ofstream(
                    logfilename.str().c_str(), std::ofstream::out | std::ofstream::app);

            if (ofs != NULL && ofs->is_open()) {
                logout = ofs;
            }
        }

        return logout;
    }

    std::ostream *outPtr = checkLoggingOutput();

    // Check what logging level to use. Default is IT::Logger::ERROR.
    //
    // If the user defines the environmental variable ITLOGLEVEL, then that
    // will be the logging output. Levels are INFO, ERROR, and FATAL.
    // Logging level is set, using bash as an example,
    // by 'export ITLOGLEVEL=INFO' (if you want INFO level logging or higher).
    // The three levels are case insensitive (info is the same as INFO, etc).
    //
    // Failures due to misspellings are silent. Default level is ERROR.
    //
    const IT::Logger::Level checkLoggingLevel(void) {
        IT::Logger::Level level = IT::Logger::ERROR;
        char *env = getenv("ITLOGLEVEL");

        if (env != NULL) {
            if (strcasecmp(env, "INFO") == 0) {
                level = IT::Logger::INFO;
            }
            else if (strcasecmp(env, "ERROR") == 0) {
                level = IT::Logger::ERROR;
            }
            else if (strcasecmp(env, "FATAL") == 0) {
                level = IT::Logger::FATAL;
            }
        }

        return level;
    }

    IT::Logger::Level levelFilter = checkLoggingLevel();

    std::ostream &setStream(std::ostream *ostr) {
        outPtr = ostr != NULL ? ostr : &std::cout;
        return *ostr;
    }

    static const char* lname[] = {
        "-  - INFO  : ",
        "!!!! ERROR : ",
        "**** FATAL : "
    };

    const char* levelToString(const IT::Logger::Level level) {
        return (level >= 0 && level < sizeof(lname)) ? lname[level] : "UNKNOWN: ";
    }
}
// end anonymous namespace

IT::Logger::Logger(std::ostream &ostr) {
    setStream(&ostr);
}

std::ostream &IT::Logger::setStream(std::ostream *ostr) {
    return setStream(ostr);
}

void IT::Logger::setLevel(const IT::Logger::Level level) {
    ::levelFilter = level;
}

// A LOG macro is wrapped around this specific function call.
// The level is one of the three IT::Logger::Levels defined in the header.
// The filename is the source file name in which this was invoked via the macro.
// The line number is the source file line number, i.e. where you
// would expect to find when you open up the source file in a text editor.
// The message is the explicit message written with the macro.
// Logging is timestamped with the system's current local time.

void IT::Logger::log(
    const Level level,
    const std::string filename,
    const int line,
    const std::string message) {

    if (level >= ::levelFilter) {
        char atime[80];
        time_t rawtime = time(NULL);
        tm *curtime = localtime(&rawtime);
        strftime(atime, sizeof(atime), "%c %Z : ", curtime);
        *outPtr << levelToString(level) << atime;
        *outPtr << filename << " : " << line << " : " << message << std::endl;
    }
}

it_logger.cpp

#include "it_logger.h"

int main(int argv, char *argc[]) {
    std::cout << "Starting..." << std::endl;
    LOG(INFO, "Info logging.");
    LOG(ERROR, "Error logging.");
    LOG(FATAL, "Fatal logging.");

    IT::Logger::setLevel(IT::Logger::ERROR);
    std::cout << std::endl;

    LOG(INFO, "Info logging 2.");
    LOG(ERROR, "Error logging 2.");
    LOG(FATAL, "Fatal logging 2.");

    return 0;
}

logtest.cpp

Usage

Include the header file in the source you want to add logging; see logtest above for examples on how to use it in code. See the notes in the code for details about the environmental variables ITLOGLEVEL and ITLOGFILE for simple tuning and controlling output. Compile and then use. I’ve tested this on RHEL 5 and Ubuntu 14.10, both with g++.