Reputation: 57
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
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:
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