¿Por qué falla la lectura de campos de estructura de registro de std::istream y cómo puedo solucionarlo?

11 minutos de lectura

¿Por que falla la lectura de campos de estructura de
πάντα ῥεῖ

Supongamos que tenemos la siguiente situación:

  • Una estructura de registro se declara de la siguiente manera

    struct Person {
        unsigned int id;
        std::string name;
        uint8_t age;
        // ...
    };
    
  • Los registros se almacenan en un archivo con el siguiente formato:

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

El archivo debe leerse para recopilar un número arbitrario de los Person registros mencionados anteriormente:

std::istream& ifs = std::ifstream("SampleInput.txt");
std::vector<Person> persons;

Person actRecord;
while(ifs >> actRecord.id >> actRecord.name >> actRecord.age) {
    persons.push_back(actRecord);
}

if(!ifs) {
    std::err << "Input format error!" << std::endl;
} 

Pregunta:

¿Qué puedo hacer para leer los valores separados que almacenan sus valores en uno? actRecord campos de variables?

Lo anterior ejemplo de código termina con errores de tiempo de ejecución:

Runtime error    time: 0 memory: 3476 signal:-1
stderr: Input format error!

  • @ 0x499602D2 Creo que es relevante. También agregaré la etiqueta c ++ 11 para abrir un campo más amplio de soluciones. Como se mencionó, la pregunta original también puede reducirse demasiado. Siéntete libre de tocarlo 😉 …

    – πάντα ῥεῖ

    12 mayo 2014 a las 21:38


  • ¿Ha encontrado una solución a esto para manejar múltiples espacios para nombres, porque creo que tengo una buena manera de manejar esto?

    – Veritas

    18 mayo 2014 a las 19:45

  • @Veritas Si tiene otra buena solución, no dude en agregar otra respuesta aquí. Esta publicación está pensada como una sesión canónica de preguntas y respuestas.

    – πάντα ῥεῖ

    18 mayo 2014 a las 19:47


  • ¿Intentaste hacer una sobrecarga de operadores?

    – padawan

    21 mayo 2014 a las 21:39

  • @OnurÇağırıcı ‘¿Intentaste hacer una sobrecarga de operadores?’ Sí, mira aquí.

    – πάντα ῥεῖ

    21 mayo 2014 a las 21:42

¿Por que falla la lectura de campos de estructura de
πάντα ῥεῖ

Una solución viable es reordenar los campos de entrada (si esto es posible)

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

y leer en los registros lo siguiente

#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;
}

  • Solución barata, pero esta es la forma más sencilla de hacer las cosas si se nos permite reordenar los campos de entrada. +1 de mi parte.

    – Veritas

    19 mayo 2014 a las 9:37

¿Por que falla la lectura de campos de estructura de
unxnut

Tiene espacios en blanco entre el nombre y el apellido. Cambie su clase para tener nombre y apellido como cadenas separadas y debería funcionar. La otra cosa que puede hacer es leer en dos variables separadas, como name1 y name2 y asignarlo como

actRecord.name = name1 + " " + name2;

  • No quiero tener cadenas separadas para nombre y apellido. Tenga en cuenta que esta pregunta pretende ser un canónico de un problema frecuente.

    – πάντα ῥεῖ

    13 de abril de 2014 a las 19:06


  • @πάνταῥεῖ El problema es que el operador de entrada >> separa en espacios en blanco. No puede leer una cadena que contenga espacios en blanco con el operador de entrada.

    – Un tipo programador

    13 de abril de 2014 a las 19:07

  • @πάνταῥεῖ La solución más simple puede ser reordenar el contenido del archivo para que el nombre sea el último, luego puede usar std::getline para obtener el nombre: std::ifs >> actRecord.id >> actRecord.age && std::getline(ifs, actRecord.name)

    – Un tipo programador

    13 de abril de 2014 a las 19:09


  • @JoachimPileborg Hmm, me gusta la propuesta que se desvía de las variables temporales. Aunque todavía me pregunto cómo se podrían manejar los nombres que contienen más partes separadas por espacios, sin colocar el nombre al final de la información del registro.

    – πάντα ῥεῖ

    13/04/2014 a las 19:13

  • @JoachimPileborg Entonces la respuesta es No (no puedes hacer nada sin más restricciones de formato)?? Sería aceptable, por lo que quise preguntar, referirme en el futuro ;)…

    – πάντα ῥεῖ

    13/04/2014 a las 19:20


1647575288 924 ¿Por que falla la lectura de campos de estructura de
david g

Aquí hay una implementación de un manipulador que se me ocurrió que cuenta el delimitador a través de cada carácter extraído. Usando la cantidad de delimitadores que especifique, extraerá palabras del flujo de entrada. Aquí hay una demostración de trabajo.

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);
}

Ahora solo puedes hacer:

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

Demo en vivo

  • Tuve al menos una idea similar, especificar un carácter delimitador de campo adicional, en lugar de especificar el número posible de partes estáticamente (?!?).

    – πάντα ῥεῖ

    13 abr 2014 a las 22:36


1647575289 977 ¿Por que falla la lectura de campos de estructura de
Ferenc Deak

Una solución sería leer en la primera entrada de un ID variable.
Luego lea todas las demás palabras de la línea (simplemente empújelas en un vector temporal) y construya el nombre del individuo con todos los elementos, excepto la última entrada que es la Edad.

Esto le permitiría tener la edad en la última posición pero poder manejar un nombre como “J. Ross Unusual”.

Actualizar para agregar un código que ilustre la teoría anterior:

#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;
    }
}

1647575289 380 ¿Por que falla la lectura de campos de estructura de
Veritas

Dado que podemos dividir fácilmente una línea en espacios en blanco y sabemos que el único valor que se puede separar es el nombre, una posible solución es usar un deque para cada línea que contenga los elementos separados por espacios en blanco de la línea. La identificación y la edad se pueden recuperar fácilmente del deque y los elementos restantes se pueden concatenar para recuperar el nombre:

#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;
}

  • THX por apoyar esta sesión de preguntas y respuestas. Su respuesta proporciona una buena solución, sin la necesidad de introducir caracteres delimitadores adicionales para una cadena. Sin embargo, al igual que la respuesta directa para poner el nombre completo al final del registro, coincide con un caso extremo, que funciona para el formato de entrada particular dado en la pregunta.

    – πάντα ῥεῖ

    19 mayo 2014 a las 18:47

  • Lo que me gusta de esto es que a veces no tienes ningún control sobre el archivo de entrada. En mi caso, un servicio web llama a mi código con datos recibidos en un formato particular.

    – Jerry Jeremías

    8 de julio de 2015 a las 0:18


Otra solución es requerir ciertos caracteres delimitadores para un campo en particular y proporcionar un manipulador de extracción especial para este propósito.

Supongamos que definimos el carácter delimitador "y la entrada debería verse así:

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

Generalmente necesario incluye:

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

La declaración de registro:

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

Declaración/definición de una clase de proxy (estructura) que admite su uso con el std::istream& operator>>(std::istream&, const delim_field_extractor_proxy&) sobrecarga del operador global:

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;
}

Plomería todo conectado e instanciando el 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;
}

Ver el ejemplo de trabajo aquí.

NOTA:

Esta solución también funciona bien especificando un carácter TAB (\t) como delimitador, que es un estándar de análisis útil .csv formatos.

  • THX por apoyar esta sesión de preguntas y respuestas. Su respuesta proporciona una buena solución, sin la necesidad de introducir caracteres delimitadores adicionales para una cadena. Sin embargo, al igual que la respuesta directa para poner el nombre completo al final del registro, coincide con un caso extremo, que funciona para el formato de entrada particular dado en la pregunta.

    – πάντα ῥεῖ

    19 mayo 2014 a las 18:47

  • Lo que me gusta de esto es que a veces no tienes ningún control sobre el archivo de entrada. En mi caso, un servicio web llama a mi código con datos recibidos en un formato particular.

    – Jerry Jeremías

    8 de julio de 2015 a las 0:18


¿Qué puedo hacer para leer en las palabras separadas que forman el nombre en el actRecord.name ¿variable?

La respuesta general es: Nono puede hacer esto sin especificaciones de delimitador adicionales y un análisis excepcional de las partes que forman el objetivo actRecord.name contenido.
Esto se debe a que un std::string El campo se analizará justo hasta la siguiente aparición de un carácter de espacio en blanco.

Es notable que algunos formatos estándar (como, por ejemplo, .csv) puede requerir para admitir espacios en blanco distintivos (' ') de la pestaña ('\t') u otros caracteres, para delimitar determinados campos del registro (que pueden no ser visibles a simple vista).

También tenga en cuenta:

para leer un uint8_t valor como entrada numérica, tendrá que desviarse usando un temporal unsigned intvalor. leyendo solo un unsigned char (también conocido como uint8_t) arruinará el estado de análisis de flujo.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad