0

I have a program where I want to load Variables from a text file to use them as default variables.

The text file should look like this:

Name=No Name

Age=8

Gender=male

etc.

Is there a simpler way and if not how do I do that in the place with the question marks?

My Code look like this:

int Age;
std::string Name;
bool male;

if(f.is_open())
{
    while (!f.eof())
    {

        getline(f, line);
        if (line.find("Name=") == std::string::npos)
        {
            Name=?????;
            continue;
        }
        else if (line.find("Gender=") == std::string::npos)
        {
            if(????? == "true"); then
               male=true;
            else
               male=false;

            continue;
        }
        else if (line.find("Age=") == std::string::npos)
        {
            Age=?????;
            continue;
        }
        //etc. ...
}
f.close();
Timisorean
  • 1,388
  • 7
  • 20
  • 30
Altinsystems
  • 186
  • 2
  • 8
  • i am a bit confused, are you asking how to fill the gaps in your code to make it work, or how to do the same differently? Asking for something "simpler" is kind of pointless unless you already have something that is working correctly – 463035818_is_not_an_ai Apr 02 '19 at 16:07
  • 4
    There are several serialization libraries you could use.. – Jesper Juhl Apr 02 '19 at 16:07
  • 6
    **[`while (!f.eof())`](https://stackoverflow.com/questions/5605125/why-is-iostreameof-inside-a-loop-condition-considered-wrong)** – πάντα ῥεῖ Apr 02 '19 at 16:09
  • You should really consider changing your file format.. Why not just values instead of `name=value` pair. Also consider using a `Person` class/struct to model the real-world persons. That way the consumers of your code can be kept away from unnecessary implementation details. – sjsam Apr 02 '19 at 16:16
  • You probably should read the docs again `find` returns `npos` if the string you are searching for was **not** found. And with `substr` you can get the part after the `=`. – t.niese Apr 02 '19 at 16:20

4 Answers4

1

Is there a simpler way?

You could use a serialization library, like cereal or Boost, as @JesperJuhl suggested.

However, I would strongly suggest to take a step back, and review your approach. You are asking for an improvement, but you don't have a good solution at this point, because Why is iostream::eof inside a loop condition considered wrong?

As I had written here, I will use std::getline() as the loop condition instead of ios::eof(), in order to parse the file, line by line.

How do I do that in the place with the question marks?

Then, for every line, I will tokenize it, based on a delimiter (equal sign in your case), in order to extract two tokens, the name of the variable and its default value. Read more about it in Parse (split) a string in C++ using string delimiter (standard C++)

Afterwards, I would use an if-else approach (You could use a switch statement instead) to check the name of the variable, and assign its default value to the actual variables of the program.

Full code example:

#include <iostream>
#include <string>
#include <fstream>

int main(void) {
  std::string defaultName, gender;
  int age;

  std::ifstream infile("mytextfile.txt");
  std::string line, varName, defaultValue;
  std::string delimiter = "=";
  while (std::getline(infile, line)) {
    varName = line.substr(0, line.find(delimiter));
    defaultValue = line.substr(line.find(delimiter) + 1);
    if(varName == "Name") {
      defaultName = defaultValue;
      continue;
    } else if(varName == "Age") {
      age = std::stoi(defaultValue);
      continue;
    } else if(varName == "Gender") {
      gender = defaultValue;
      continue;
    } else {
      std::cout << "Unknown entry: " << line << std::endl;
    }
  }

  std::cout << defaultName << ", " << age << ", " << gender << std::endl;

  return 0;
}

Output:

No Name, 8, male
gsamaras
  • 71,951
  • 46
  • 188
  • 305
  • 1
    If you add "else" in front of each "if" after the firts , you can use: else if( varName != defaultValue ), to check unkonown var names. Right? – Altinsystems Apr 03 '19 at 14:25
  • Right @Altinsystems, and I recommend that, answer updated! – gsamaras Apr 03 '19 at 15:51
  • 1
    But now if there is a empty line, there is a unknown entry, too. I added if(varName == "" /* || defaultValue == "" */ ) { continue; } at the beginning. And if a line doesnt contains a =: varName and defaultValue are the same ... so a check at the end: if( varName != defaultValue ), to test for an unknown var – Altinsystems Apr 03 '19 at 17:01
1

If you feel a need to write it yourself instead of using a ready library, you could use a std::unordered_map<> and add some streaming and extraction support around it. Here's an example with comments in the code:

#include <string>
#include <unordered_map>

class KeyValue { //        Key          Value    
    std::unordered_map<std::string, std::string> m_kv{};

public:
    // at() is used to get a reference to a Value given the supplied Key. It uses
    // the function with the same name in the unordered_map.

    inline std::string& at(const std::string& Key) { return m_kv.at(Key); }
    inline const std::string& at(const std::string& Key) const { return m_kv.at(Key); }

    // The "as<T>" function below is used to extract values from the map.
    // The exact version of the function that will be used depends on the type
    // you want to extract from the string. Explicit specializations of the function
    // are declared outside the class.

    // A generic conversion function to anything that can be constructed from a std::string
    template<typename T>
    T as(const std::string& Key) const {
        return at(Key);
    }

    // A function to extract directly into a variable using the proper as<T>
    template<typename T>
    void extract_to(T& var, const std::string& Key) const {
        var = as<T>(Key);
    }

    // A friend function to read from an input stream (like an open file) and
    // populate the unordered_map.
    friend std::istream& operator>>(std::istream&, KeyValue&);
};

// Explicit specializations of KeyValue::as<T>()

// floats
template<>
float KeyValue::as(const std::string& Key) const {
    return std::stof(at(Key));
}

template<>
double KeyValue::as(const std::string& Key) const {
    return std::stod(at(Key));
}

template<>
long double KeyValue::as(const std::string& Key) const {
    return std::stold(at(Key));
}
// signed integers
template<>
int KeyValue::as(const std::string& Key) const {
    return std::stoi(at(Key));
}

template<>
long KeyValue::as(const std::string& Key) const {
    return std::stol(at(Key));
}

template<>
long long KeyValue::as(const std::string& Key) const {
    return std::stoll(at(Key));
}
// unsigned integers
template<>
unsigned KeyValue::as(const std::string& Key) const {
    return std::stoul(at(Key));
}

template<>
unsigned long KeyValue::as(const std::string& Key) const {
    return std::stoul(at(Key));
}

template<>
unsigned long long KeyValue::as(const std::string& Key) const {
    return std::stoull(at(Key));
}
// bool
template<>
bool KeyValue::as(const std::string& Key) const {
    const std::string& val = at(Key);
    if(val=="true" || val=="1") return true;
    else if(val=="false" || val=="0") return false;
    throw std::range_error("\"" + Key + "\" is neither true nor false");
}   

// the friend function that extracts key value strings from a stream
std::istream& operator>>(std::istream& is, KeyValue& kv) {
    std::string line;

    // read one line at a time
    while(std::getline(is, line)) {
        auto pos = line.find('=');
        if(pos == std::string::npos || pos == 0) {
            // if '=' was not found (or found at pos 0), set the failbit on the stream
            is.setstate(std::ios::failbit);
        } else {
            // if '=' was found, put the Key and Value in the map by
            // using substr() to split the line where the '=' was found
            kv.m_kv.emplace(line.substr(0, pos), line.substr(pos + 1));
        }
    }
    return is;
}

With that in place, you can read a file and populate the variables that you've preferably put in a class / struct. Example:

#include <fstream>

struct Variables {
    std::string Name{};
    unsigned int Age{};
    std::string Gender{};
    double PI{};
    bool Hungry{};
    bool Sad{};

    Variables(const std::string& filename) {
        std::ifstream is(filename);
        if(is) {
            KeyValue tmp;
            is >> tmp; // stream the whole file into tmp

            // extract values
            tmp.extract_to(Name, "Name");
            tmp.extract_to(Age, "Age");
            tmp.extract_to(Gender, "Gender");
            tmp.extract_to(PI, "PI");
            tmp.extract_to(Hungry, "Hungry");
            tmp.extract_to(Sad, "Sad");
        } else throw std::runtime_error("Could not read \""+filename+"\".");
    }
};

Example data file (vars.dat):

Name=No name
Age=8
Gender=male
PI=3.14159
Hungry=true
Sad=false

...and a main example::

#include <iostream>

int main() {
    try {
        Variables var("vars.dat"); // open file and populate variables

        std::cout << std::boolalpha
            << "Name:   " << var.Name << "\n"
            << "Age:    " << var.Age << "\n"
            << "Gender: " << var.Gender << "\n"
            << "PI:     " << var.PI << "\n"
            << "Hungry: " << var.Hungry << "\n"
            << "Sad:    " << var.Sad << "\n";

    } catch(const std::exception& ex) {
        std::cerr << ex.what() << "\n";
    }
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • If I read variables from text files in my main program, I would use this solution. But first I did not understand this solution properly and secondly I just need to set the start value of my variable. But this solution is quite cumbersome. – Altinsystems Apr 30 '19 at 18:19
0

I tried to simplify the solution of @Ted Lyngmo: ... I think it is not the fastest way and not the best, but it is more simple and more short:

#include <sstream>

class loadVars
{
public:
    std::string file;
    loadVars() { }

    //Input ->
    loadVars(std::string Text) {
        this->setFile(Text);
    }

    loadVars(std::istream& is) {
        this->setFile(is);
    }

    friend void operator>>(std::istream& is, loadVars& lv) {
        lv.file = std::string((std::istreambuf_iterator<char>(is)), std::istreambuf_iterator<char>());
    }

    void setFile(std::string Text) {
        this->file = Text;
    }

    void setFile(std::istream& is) {
        this->file = std::string((std::istreambuf_iterator<char>(is)), std::istreambuf_iterator<char>());
    }
    //<-

    std::string extract_to_first(std::string to) {
        std::string line;
        std::stringstream s_string = std::stringstream(this->file);

        while (std::getline(s_string, line)) {
            if(line.find("=") != std::string::npos) {
                if(line.substr(0,line.find("=")) == to) {
                    return line.substr(line.find("=")+1);
                }
            }
        }
        return "-1";
    }

};
Altinsystems
  • 186
  • 2
  • 8
-1

I would not reinvent this. As suggested, libraries for serialization exist. Consider Boost.PropertyTree as an example and Boost can be helpful to learn in general.