user1941332
user1941332

Reputation: 57

A simple way to read TXT config files in C++

this question seems to be already asked but I did not find any convenient solution for my case. I have the following TXT config file to read in C++:

--CONFIGURATION 1 BEGIN--

IP address:                          192.168.1.145
Total track length [m]:              1000
Output rate [1/s]:                   10
Time [s]:                            1
Running mode (0=OFF 1=ON):           1
Total number of attempts:            10
Mode (0=OFF, 1=BEG, 2=ADV, 3=PROF):  1

--Available only for Administrators--

Variable 1 [mV]:                     2600
Gain 1 [mV]:                         200
Position tracking (0=OFF 1=ON):      0
Coefficient 2 [V]:                   5.2

--CONFIGURATION 1 END--

--CONFIGURATION 2 BEGIN--

Max track distance [m]:             10000
Internal track length [m]:          100
Offset distance [mV]:               1180
GAIN bias [mV]:                     200
Number of track samples:            1000
Resolution (1 or 2) [profile]:      1

--CONFIGURATION 2 END--

I need to store only the value at the end of each line that could be a string (in the case of the IP address), an int, a float or a bool inside a struct. In C there is a very simple solution, I read each single line using an expression as follows:

if(!fscanf(fp, "%*s %*s %*s %*s %d\n", &(settings->trackLength))) {

    printf("Invalid formatting of configuration file. Check trackLength.\n");
    return -1;
}

The %*s allows to discard the label of the line and the spaces before the interested value. I use fgets to skip the empty lines or the titles. This way works also in C++. Is it good to leave my code as is or do you see a better and simple way to do this in C++? Thank you very much.

Upvotes: 1

Views: 4888

Answers (1)

A M
A M

Reputation: 15277

Also in C++ it is easy to split a line. I have already provided several answers here on SO on how to split a string. Anyway, I will explain it here in detail and for your special case. I also provide a full working example later.

We use the basic functionality of std::getline which can read a complete line or the line up to a given character. Please see here.

Let us take an example. If the text is stored in a std::string we will first put it into a std::istringstream. Then we can use std::getline to extract the data from the std::istringstream. That is always the standard approach. First, read the complete line from a file using std::getline, then, put it in a std::istringstream again, to be able extract the parts of the string again with std::getline.

If a source line looks like that:

Time [s]:                            1

We can obsserve that we have several parts:

  • An identifier "Time [s]",
  • a colon, which acts as a separator,
  • one or more spaces and
  • the value "1"

So, we could write something like this:

std::string line{};  // Here we will store a complete line read from the source file
std::getline(configFileStream, line);  // Read a complete line from the source file
std::istringstream iss{ line };  // Put line into a istringstream for further extraction

std::string id{};  // Here we will store the target value "id"
std::string value{};   // Here we will store the target "value"
std::getline(iss, id, ':');  // Read the ID, get read of the colon
iss >> std::ws;  // Skip all white spaces
std::getline(iss, value);  // Finally read the value

So, that is a lot of text. You may have heard that you can chain IO-Operations, like in std::cout << a << b << c. This works, because the << operation always returns a reference to the given stream. And the same is true for std::getline. And because it does this, we can use nested statements. Meaning, we can put the second std::getline at this parameter position (actually the first paramater) where it expects a std::istream. If we follow this approach consequently then we can write the nested statement:

std::getline(std::getline(iss, id, ':') >> std::ws, value);

Ooops, whats going on here? Let's analyze from inside out. First the operation std::getline(iss, id, ':') extracts a string from the std::istringstream and assign it to variable "id". OK, understood. Remember: std::getline, will return a reference to the given stream. So, then the above reduced statement is

std::getline(iss >> std::ws, value)

Next, iss >> std::ws will be evaluated and will result in eating up all not necessary white spaces. And guess what, it will return a refernce to the gievn stream "iss".

Statement looks now like:

std::getline(iss, value)

And this will read the value. Simple.

But, we are not finished yet. Of course std::getline will return again "iss". And in the below code, you will see something like

if (std::getline(std::getline(iss, id, ':') >> std::ws, value))

which will end up as if (iss). So, we use iss as a boolean expression? Why does this work and what does it do? It works, because the bool operator of the std::stream is overwritten and returns, if the state is OK or has a failure. Please see here for an explanation. Always check the result of any IO-operation.

And last but not least, we need to explain the if statement with initializer. You can read about it here.

I can write

if (std::string id{}, value{}; std::getline(std::getline(iss, id, ':') >> std::ws, value)) {

which is the similar to

std::string id{}, value{}; 
if (std::getline(std::getline(iss, id, ':') >> std::ws, value)) {

But the first example has the advantage that the defined variables will be only visible within the if-statements scope. So, we "scope" the variable as narrow as possible.

You should try to do that as often as possible. You should also always check the return state of an IO-operation by applying if to a stream-operation, as shown above.

The complete program for reading everything will then just be a few lines of code.

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

int main() {

    // Open config file and check, if it coul be opened
    if (std::ifstream configFileStream{ "r:\\config.txt" }; configFileStream) {

        // Here we wills tore the resulting config data
        std::unordered_map<std::string, std::string> configData;

        // Read all lines of the source file
        for (std::string line{}; std::getline(configFileStream, line); )
        {
            // If the line contains a colon, we treat it as valid data
            if (if (line.find(':') != std::string::npos)) {

                // Split data in line into an id and a value part and save it
                std::istringstream iss{ line };
                if (std::string id{}, value{}; std::getline(std::getline(iss, id, ':') >> std::ws, value)) {

                    // Add config data to our map
                    configData[id] = value;
                }
            }
        }
        // Some debug output
        for (const auto& [id, value] : configData)
            std::cout << "ID: " << std::left << std::setw(35) << id << " Value: " << value << '\n';
    }
    else std::cerr << "\n*** Error: Could not open config file for reading\n";

    return 0;
}

For this example I store the ids and values in a map, so that they can be accessed easily.

Upvotes: 5

Related Questions