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

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 usando 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 recostackr una cantidad arbitraria de los registros de Person mencionados anteriormente:

 std::istream& ifs = std::ifstream("SampleInput.txt"); std::vector 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: (esa es una pregunta frecuente, en una u otra forma)
¿Qué puedo hacer para leer en los valores separados que almacenan sus valores en los actRecord variables actRecord ?

La muestra de código anterior termina con errores de tiempo de ejecución:

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

Una solución viable es reordenar 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 lea en los registros de la siguiente manera

 #include  #include  struct Person { unsigned int id; std::string name; uint8_t age; // ... }; int main() { std::istream& ifs = std::cin; // Open file alternatively std::vector 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; } 

Tiene espacios en blanco entre firstname y lastname. Cambie su clase para tener firstname y lastname como cadenas separadas y debería funcionar. La otra cosa que puede hacer es leer en dos variables separadas como name1 y name2 y asignarla como

 actRecord.name = name1 + " " + name2; 

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 de la secuencia de entrada. Aquí hay una demostración funcional.

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

Ahora 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

Una solución sería leer en la primera entrada en una variable de ID .
Luego, lea todas las otras 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 el último puesto pero poder tratar el nombre como “J. Ross Unusual”.

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

 #include  #include  #include  #include  #include  #include  #include  struct Person { unsigned int id; std::string name; int age; }; int main() { std::fstream ifs("in.txt"); std::vector 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 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(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::const_iterator it = persons.begin(); it != persons.end(); it++) { std::cout << it->id << ":" << it->name << ":" << it->age << std::endl; } } 

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  #include  #include  #include  #include  #include  #include  #include  #include  struct Person { unsigned int id; std::string name; uint8_t age; }; 

 int main(int argc, char* argv[]) { std::ifstream ifs("SampleInput.txt"); std::vector records; std::string line; while (std::getline(ifs,line)) { std::istringstream ss(line); std::deque info(std::istream_iterator(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(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(record.age) << std::endl; } return 0; } 

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

Supongamos que definimos el carácter del 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 se necesita incluir:

 #include  #include  #include  

La statement de registro:

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

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

 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 entre sí y delim_field_extractor_proxy instancia del delim_field_extractor_proxy :

 int main() { std::istream& ifs = std::cin; // Open file alternatively std::vector 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; } 

Vea el ejemplo de trabajo aquí .

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

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

La respuesta general es: No , no puede hacer esto sin especificaciones de delimitadores adicionales y un análisis excepcional para las partes que forman los contenidos previstos de actRecord.name .
Esto se debe a que un campo std::string será analizado justo hasta la próxima ocurrencia de un carácter de espacio en blanco.

Cabe destacar que algunos formatos estándar (como, por ejemplo, .csv ) pueden requerir para admitir espacios en blanco ( ' ' ) de tabulación ( '\t' ) u otros caracteres, para delimitar ciertos campos de registro (que pueden no ser visibles a primera vista) .

También tenga en cuenta:
Para leer un valor de uint8_t como entrada numérica, deberá desviarse utilizando un valor unsigned int temporal unsigned int . Leer solo un unsigned char (alias uint8_t ) uint8_t el estado de análisis de flujo.

Otro bash de resolver el problema de análisis.

 int main() { std::ifstream ifs("test-115.in"); std::vector persons; while (true) { Person actRecord; // Read the ID and the first part of the name. if ( !(ifs >> actRecord.id >> actRecord.name ) ) { break; } // Read the rest of the line. std::string line; std::getline(ifs,line); // Pickup the rest of the name from the rest of the line. // The last token in the rest of the line is the age. // All other tokens are part of the name. // The tokens can be separated by ' ' or '\t'. size_t pos = 0; size_t iter1 = 0; size_t iter2 = 0; while ( (iter1 = line.find(' ', pos)) != std::string::npos || (iter2 = line.find('\t', pos)) != std::string::npos ) { size_t iter = (iter1 != std::string::npos) ? iter1 : iter2; actRecord.name += line.substr(pos, (iter - pos + 1)); pos = iter + 1; // Skip multiple whitespace characters. while ( isspace(line[pos]) ) { ++pos; } } // Trim the last whitespace from the name. actRecord.name.erase(actRecord.name.size()-1); // Extract the age. // std::stoi returns an integer. We are assuming that // it will be small enough to fit into an uint8_t. actRecord.age = std::stoi(line.substr(pos).c_str()); // Debugging aid.. Make sure we have extracted the data correctly. std::cout << "ID: " << actRecord.id << ", name: " << actRecord.name << ", age: " << (int)actRecord.age << std::endl; persons.push_back(actRecord); } // If came here before the EOF was reached, there was an // error in the input file. if ( !(ifs.eof()) ) { std::cerr << "Input format error!" << std::endl; } } 

Al ver un archivo de entrada de este tipo, creo que no es un archivo delimitado (de nueva manera), sino un campo de campos de tamaño fijo antiguo, como los progtwigdores de Fortran y Cobol solían tratar. Entonces lo analizaría de esa manera (nota que separé el nombre y el apellido):

 #include  #include  #include  #include  #include  struct Person { unsigned int id; std::string forename; std::string lastname; uint8_t age; // ... }; int main() { std::istream& ifs = std::ifstream("file.txt"); std::vector persons; std::string line; int fieldsize[] = {8, 9, 9, 4}; while(std::getline(ifs, line)) { Person person; int field = 0, start=0, last; std::stringstream fieldtxt; fieldtxt.str(line.substr(start, fieldsize[0])); fieldtxt >> person.id; start += fieldsize[0]; person.forename=line.substr(start, fieldsize[1]); last = person.forename.find_last_not_of(' ') + 1; person.forename.erase(last); start += fieldsize[1]; person.lastname=line.substr(start, fieldsize[2]); last = person.lastname.find_last_not_of(' ') + 1; person.lastname.erase(last); start += fieldsize[2]; std::string a = line.substr(start, fieldsize[3]); fieldtxt.str(line.substr(start, fieldsize[3])); fieldtxt >> age; person.age = person.age; persons.push_back(person); } return 0; }