Why does reading a record struct fields from std::istream fail, and how can I fix it?

后端 未结 9 2132
野性不改
野性不改 2020-11-21 11:40

Suppose we have the following situation:

  • A record struct is declared as follows

struct Person {
    unsigned int id;
    std::st         


        
相关标签:
9条回答
  • 2020-11-21 12:09

    Since we can easily split a line on whitespace and we know that the only value that can be separated is the name, a possible solution is to use a deque for each line containing the whitespace separated elements of the line. The id and the age can easily be retrieved from the deque and the remaining elements can be concatenated to retrieve the name:

    #include <iostream>
    #include <fstream>
    #include <deque>
    #include <vector>
    #include <sstream>
    #include <iterator>
    #include <string>
    #include <algorithm>
    #include <utility>
    
    struct Person {
        unsigned int id;
        std::string name;
        uint8_t age;
    };
    

    int main(int argc, char* argv[]) {
    
        std::ifstream ifs("SampleInput.txt");
        std::vector<Person> records;
    
        std::string line;
        while (std::getline(ifs,line)) {
    
            std::istringstream ss(line);
    
            std::deque<std::string> info(std::istream_iterator<std::string>(ss), {});
    
            Person record;
            record.id = std::stoi(info.front()); info.pop_front();
            record.age = std::stoi(info.back()); info.pop_back();
    
            std::ostringstream name;
            std::copy
                ( info.begin()
                , info.end()
                , std::ostream_iterator<std::string>(name," "));
            record.name = name.str(); record.name.pop_back();
    
            records.push_back(std::move(record));
        }
    
        for (auto& record : records) {
            std::cout << record.id << " " << record.name << " " 
                      << static_cast<unsigned int>(record.age) << std::endl;
        }
    
        return 0;
    }
    
    0 讨论(0)
  • 2020-11-21 12:10

    Here's an implementation of a manipulator I came up with that counts the delimiter through each extracted character. Using the number of delimiters you specify, it will extract words from the input stream. Here's a working demo.

    template<class charT>
    struct word_inserter_impl {
        word_inserter_impl(std::size_t words, std::basic_string<charT>& str, charT delim)
            : str_(str)
            , delim_(delim)
            , words_(words)
        { }
    
        friend std::basic_istream<charT>&
        operator>>(std::basic_istream<charT>& is, const word_inserter_impl<charT>& wi) {
            typename std::basic_istream<charT>::sentry ok(is);
    
            if (ok) {
                std::istreambuf_iterator<charT> it(is), end;
                std::back_insert_iterator<std::string> dest(wi.str_);
    
                while (it != end && wi.words_) {
                    if (*it == wi.delim_ && --wi.words_ == 0) {
                        break;
                    }
                    dest++ = *it++;
                }
            }
            return is;
        }
    private:
        std::basic_string<charT>& str_;
        charT delim_;
        mutable std::size_t words_;
    };
    
    template<class charT=char>
    word_inserter_impl<charT> word_inserter(std::size_t words, std::basic_string<charT>& str, charT delim = charT(' ')) {
        return word_inserter_impl<charT>(words, str, delim);
    }
    

    Now you can just do:

    while (ifs >> actRecord.id >> word_inserter(2, actRecord.name) >> actRecord.age) {
        std::cout << actRecord.id << " " << actRecord.name << " " << actRecord.age << '\n';
    }
    

    Live Demo

    0 讨论(0)
  • 2020-11-21 12:13

    One viable solution is to reorder input fields (if this is possible)

    ID      Age Forename Lastname
    1267867 32  John     Smith    
    67545   36  Jane     Doe      
    8677453 56  Gwyneth  Miller   
    75543   23  J. Ross  Unusual  
    ...
    

    and read in the records as follows

    #include <iostream>
    #include <vector>
    
    struct Person {
        unsigned int id;
        std::string name;
        uint8_t age;
        // ...
    };
    
    int main() {
        std::istream& ifs = std::cin; // Open file alternatively
        std::vector<Person> persons;
    
        Person actRecord;
        unsigned int age;
        while(ifs >> actRecord.id >> age && 
              std::getline(ifs, actRecord.name)) {
            actRecord.age = uint8_t(age);
            persons.push_back(actRecord);
        }
    
        return 0;
    }
    
    0 讨论(0)
  • 2020-11-21 12:13

    You have whitespace between firstname and lastname. Change your class to have firstname and lastname as separate strings and it should work. The other thing you can do is to read in two separate variables such as name1 and name2 and assign it as

    actRecord.name = name1 + " " + name2;
    
    0 讨论(0)
  • 2020-11-21 12:13

    A solution would be to read in the first entry into an ID variable.
    Then read in all the other words from the line (just push them in a temporary vector) and construct the name of the individual with all the elements, except the last entry which is the Age.

    This would allow you to still have the Age on the last position but be able to deal with name like "J. Ross Unusual".

    Update to add some code which illustrates the theory above:

    #include <memory>
    #include <string>
    #include <vector>
    #include <iterator>
    #include <fstream>
    #include <sstream>
    #include <iostream>
    
    struct Person {
        unsigned int id;
        std::string name;
        int age;
    };
    
    int main()
    {
        std::fstream ifs("in.txt");
        std::vector<Person> persons;
    
        std::string line;
        while (std::getline(ifs, line))
        {
            std::istringstream iss(line);
    
            // first: ID simply read it
            Person actRecord;
            iss >> actRecord.id;
    
            // next iteration: read in everything
            std::string temp;
            std::vector<std::string> tempvect;
            while(iss >> temp) {
                tempvect.push_back(temp);
            }
    
            // then: the name, let's join the vector in a way to not to get a trailing space
            // also taking care of people who do not have two names ...
            int LAST = 2;
            if(tempvect.size() < 2) // only the name and age are in there
            {
                LAST = 1;
            }
            std::ostringstream oss;
            std::copy(tempvect.begin(), tempvect.end() - LAST,
                std::ostream_iterator<std::string>(oss, " "));
            // the last element
            oss << *(tempvect.end() - LAST);
            actRecord.name = oss.str();
    
            // and the age
            actRecord.age = std::stoi( *(tempvect.end() - 1) );
            persons.push_back(actRecord);
        }
    
        for(std::vector<Person>::const_iterator it = persons.begin(); it != persons.end(); it++)
        {
            std::cout << it->id << ":" << it->name << ":" << it->age << std::endl;
        }
    }
    
    0 讨论(0)
  • 2020-11-21 12:15

    Another solution is to require certain delimiter characters for a particular field, and provide a special extraction manipulator for this purpose.

    Let's suppose we define the delimiter character ", and the input should look like this:

    1267867 "John Smith"      32   
    67545   "Jane Doe"        36  
    8677453 "Gwyneth Miller"  56  
    75543   "J. Ross Unusual" 23  
    

    Generally needed includes:

    #include <iostream>
    #include <vector>
    #include <iomanip>
    

    The record declaration:

    struct Person {
        unsigned int id;
        std::string name;
        uint8_t age;
        // ...
    };
    

    Declaration/definition of a proxy class (struct) that supports being used with the std::istream& operator>>(std::istream&, const delim_field_extractor_proxy&) global operator overload:

    struct delim_field_extractor_proxy { 
        delim_field_extractor_proxy
           ( std::string& field_ref
           , char delim = '"'
           ) 
        : field_ref_(field_ref), delim_(delim) {}
    
        friend 
        std::istream& operator>>
           ( std::istream& is
           , const delim_field_extractor_proxy& extractor_proxy);
    
        void extract_value(std::istream& is) const {
            field_ref_.clear();
            char input;
            bool addChars = false;
            while(is) {
                is.get(input);
                if(is.eof()) {
                    break;
                }
                if(input == delim_) {
                    addChars = !addChars;
                    if(!addChars) {
                        break;
                    }
                    else {
                        continue;
                    }
                }
                if(addChars) {
                    field_ref_ += input;
                }
            }
            // consume whitespaces
            while(std::isspace(is.peek())) {
                is.get();
            }
        }
        std::string& field_ref_;
        char delim_;
    };
    

    std::istream& operator>>
        ( std::istream& is
        , const delim_field_extractor_proxy& extractor_proxy) {
        extractor_proxy.extract_value(is);
        return is;
    }
    

    Plumbing everything connected together and instantiating the delim_field_extractor_proxy:

    int main() {
        std::istream& ifs = std::cin; // Open file alternatively
        std::vector<Person> persons;
    
        Person actRecord;
        int act_age;
        while(ifs >> actRecord.id 
                  >> delim_field_extractor_proxy(actRecord.name,'"')
                  >> act_age) {
            actRecord.age = uint8_t(act_age);
            persons.push_back(actRecord);
        }
    
        for(auto it = persons.begin();
            it != persons.end();
            ++it) {
            std::cout << it->id << ", " 
                          << it->name << ", " 
                          << int(it->age) << std::endl;
        }
        return 0;
    }
    

    See the working example here.

    NOTE:
    This solution also works well specifying a TAB character (\t) as delimiter, which is useful parsing standard .csv formats.

    0 讨论(0)
提交回复
热议问题