LarsA
LarsA

Reputation: 637

Exception propagation and context

What is the recommended practice to good context information in an exception? To illustrate here is an simplified example based on experienced.

#include <iostream>
#include <fstream>
#include <boost/filesystem.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/uuid/string_generator.hpp>
#include <boost/algorithm/string.hpp>

class object
{
public:
  void parse(std::istream& is)
  {
    std::istreambuf_iterator<char> eos;
    std::string buf(std::istreambuf_iterator<char>(is), eos);

    std::vector<std::string> list;
    boost::split(list, buf, boost::is_any_of(","), boost::algorithm::token_compress_on);

    for (auto str : list)
    {
      auto id = boost::uuids::string_generator()(str);
    }
  }
};

void foo()
{
  {
    boost::filesystem::ifstream is("foo.txt");
    object obj;
    obj.parse(is);
  }

  {
    boost::filesystem::ifstream is("bar.txt");
    object obj;
    obj.parse(is);
  }
}

int main()
{
  try
  {
    foo();
  }
  catch (const std::exception& ex)
  {
    std::cout << ex.what() << std::endl;
  }

  return 0;
}

This example reads multiple uuids from two files. If one uuid is poorly formatted an exception is thrown and the output message is "invalid uuid string" which is correct.

In this simple and static example this is probably enough information to figure out the problem but in a more dynamic and complex situation it might not be enough. It would be nice to know which file that contained the invalid uuid and possibly which uuid that failed to parse. Obviously boost::uuid::string_generator cannot provide all that information. One approach would be to catch the original exception early and rethrow with more information but in this case I would need to do that twice to first get "uuid" value and then get "filename".

Appreciate any input on the matter.

Upvotes: 2

Views: 124

Answers (2)

atru
atru

Reputation: 4744

You can create a separate class that parses and handles related exceptions. I've no experience with boost and functionality in your example so mine doesn't use it - it still demonstrates the concept,

#include<iostream>
#include <fstream>
#include <exception>
#include <vector>
#include <string>

// Parses a file, collects data if correct,
// if not throws an exception
class Parser
{
public:
    // Creates Parser object for parsing file file_name
    // verbose indicates whether more detailed exception message
    // should be printed
    Parser(const std::string file_name, const bool verbose):
            fname(file_name), print_ex_info(verbose) { }
    // Parsing wrapper that calls actual parsing function
    // and handles/prints exceptions
    void parse();
    // Retrieve parsed data
    std::vector<std::string> get_data() const { return data; }
private:
    std::vector<std::string> data;
    std::string fname = {};
    bool print_ex_info = true;
    // Actual parsing
    void parse_private(); 
};

void Parser::parse()
{
    try{
        parse_private();
    } catch(const std::exception& ex) {
        if (print_ex_info){
            std::cout << "File " << fname 
                      << " thrown an exception " 
                      << ex.what() << std::endl;
        }else{
            std::cout << ex.what() << std::endl;
        }
    }
}

// Throws if file contains an entry that 
// is not a positive integer 
// (for simple demonstration)
void Parser::parse_private()
{
    std::ifstream in(fname);
    std::string entry;
    while (in >> entry){
        if (entry.find_first_not_of("0123456789") != std::string::npos){
            throw std::runtime_error("Invalid entry " + entry + "\n");
        }else{
            data.push_back(entry);  
        }
    }
}

// Retrieves and uses data parsed from a file 
class Object
{
public:
    void parse(const std::string file_name, const bool verbose)
    {
        Parser file_parser(file_name, verbose);
        file_parser.parse();
        parsed_data = file_parser.get_data();
    }
    void print_parsed_data()
    { 
        for (const auto& entry : parsed_data)
            std::cout << entry << " ";
        std::cout << std::endl;
    }
private:
    std::vector<std::string> parsed_data;
};

int main()
{
    Object obj;
    bool verbose = true;

    // Correct input case
    std::cout << "No exception:\n";
    obj.parse("parser_no_ex.txt", verbose);
    obj.print_parsed_data();
    std::cout << "\n";

    // Invalid input, detailed exception info
    std::cout << "Exception - verbose version:\n";
    obj.parse("parser_invalid.txt", verbose);

    // Invalid input, reduced exception info
    std::cout << "Exception - minimal version:\n";
    verbose = false;
    obj.parse("parser_invalid.txt", verbose);

    return 0;
}

Here Object serves as an intermediate class that retrieves and uses parsed data. The parsed data is generated in the Parser object that also performs data checking and throws exceptions. This way the code that uses the data is not cluttered with parsing-related exception handling - this holds for both code that uses Object and functionality of Object itself.

For similar (cluttering, readability) reasons Parser has an extra code layer for parsing - the public function parse() has the exception handling code and makes the call to the actual, private parsing function that has no handling code but can throw.

In addition parsing has a verbose option that controls the amount of information the user wants to see after an exception is thrown. It's not a bad idea to be able to switch off more informative exceptions.

For reference these are the files used in this demonstration,

File with correct input (all positive integers) - parser_no_ex.txt:

123
456
900000
111
00

File with incorrect input - parser_invalid.txt:

123
456789
***Hello***
345

Upvotes: 1

Tagger5926
Tagger5926

Reputation: 442

Yes, if you need more information for debugging, catch and then re-throw with a more informative error message.

void tryParse(string const &filename)
{
    boost::filesystem::ifstream is(filename);
    object obj;
    try{
        obj.parse(is);
    }
    catch(const std::exception& ex) {
        throw "Exception occurred whilst parsing " + filename + ex.what());
    }
}

void foo()
{
    vector<string> files = { "foo.txt", "bar.txt" };
    for(auto const& f : files )
    {
         tryParse(f);
    }
}

You could use std::optional instead, depending on which standard of c++ you are using.

This also depends on how you are logging your errors. If you have some sort of logging mechanism, you can log the exception message and filename when the exception occurs rather than propagating the information up.

Upvotes: 1

Related Questions