c++ – Console-Based Quiz Application

I wanted to practice more object-oriented programming and decided to write a console based Quiz Application. In one of my previous question, a reviewer left some links on SOLID design pattern, I went through it and tried as much as possible to follow it to the best of my ability.

  1. I used boost library to serialize my object and I noticed my executable size increased quite a lot. I would appreciate if any one could give suggestion on how to save my objects without boost dependency, links would be preferable.
  2. I really considered removing the default constructor from my classes, but eventually left them since a container must be initialized if no default constructor is given. Does this show a poor class design?
  3. Some of my method functions were supposed to return a value for example get(), but in a scenario where an object of type Quiz was not found, I decided to throw an error. I recieved several warnings from g++ compiler, is it a poor design to throw, if no return value was produced in a function?
#ifndef QUIZ_QUIZMAKER_H_
#define QUIZ_QUIZMAKER_H_

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

#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/vector.hpp>

namespace QuizMaker
{
    struct Question
    {
        friend std::ostream& operator<<( std::ostream &os, const Question &question );
        public:
            Question() = default;
            Question( const std::string &title, const std::string &question )
                : title_{ title }, question_{ question } {}

            std::string title_;
            std::string question_;

            private:
                friend class boost::serialization::access;
                template<typename Archive>
                void serialize( Archive &archive, const unsigned int version )
                {
                    archive & title_;
                    archive & question_;
                }
    };

    struct QuestionOptions{
        friend std::ostream& operator<<( std::ostream &os, const QuestionOptions &options );
        public:
            QuestionOptions() = default;
            QuestionOptions( const std::vector<std::string> &options, const std::string &correct_option )
                : options_{ options }, correct_option_{ correct_option } {}

            std::vector<std::string> options_;
            std::string correct_option_;

        private:
            friend class boost::serialization::access;
            template<typename Archive>
            void serialize( Archive &archive, const unsigned int version )
            {
                archive & options_;
                archive & correct_option_;
            }
    };

    struct TagContainer
    {
        friend std::ostream& operator<<( std::ostream &os, const TagContainer &tag_c );
        TagContainer() = default;
        explicit TagContainer( const std::vector<std::string> &tag_c ) : tag_container_{ tag_c } {}
        std::vector<std::string> tag_container_;
        
        private:
            friend class boost::serialization::access;
            template<typename Archive>
            void serialize( Archive &archive, const unsigned int version )
            {
                archive & tag_container_;
            }
    };

    struct Quiz
    {
        public:
            Quiz() = default;
            Quiz( const Question &question, const QuestionOptions &options, const TagContainer &tag_c )
                : quiz_{ question }, quiz_options_( options ), quiz_tag_{ tag_c } {}
            Quiz( std::ifstream &file_handle );
            Question quiz_;
            QuestionOptions quiz_options_;
            TagContainer quiz_tag_;

            private:
                friend class boost::serialization::access;
                template<typename Archive>
                void serialize( Archive &archive, const unsigned int version )
                {
                    archive & quiz_;
                    archive & quiz_options_;
                    archive & quiz_tag_;
                }
    };

    std::ostream& operator<<( std::ostream &os, const Question &question );
    std::ostream& operator<<( std::ostream &os, const QuestionOptions &options );
}
        

#endif

#include "Quiz_Maker.h"

#include "Exception.h"

#include <sstream>
#include <algorithm>

namespace QuizMaker 
{
    std::ostream& operator<<( std::ostream& os, const Question& question )
    {
        os << "Title: " << question.title_  << 'n';
        os << question.question_;

        return os;
    }

    std::ostream& operator<<( std::ostream& os, const QuestionOptions& opt )
    {
        int alphabelt = 65;
        for( const auto options : opt.options_ )
            os << char( alphabelt++ ) << ". " << options << 'n';

        return os;
    }

    std::ostream& operator<<( std::ostream &os, const TagContainer &tag_c )
    {
        for( const auto tag : tag_c.tag_container_ )
            os << "(" << tag << ") ";

        return os;
    }
}  

#ifndef QUIZ_EXCEPTION_H_
#define QUIZ_EXCEPTION_H_

#include <fstream>

struct Exception
{
    bool ofile_handle_error( std::ofstream &file_handle );
    bool ifile_handle_error( std::ifstream &file_handle );
    void throw_get_error() { throw std::invalid_argument( "Access error: Record not found." ); }
};

#endif

#include "Exception.h"

#include <iostream>

bool Exception::ofile_handle_error( std::ofstream &file_handle )
{
    if( !file_handle ) {
        std::cerr << "Write Error: File cannot be accessed.";
        return true;
    }
    return false;
}

bool Exception::ifile_handle_error( std::ifstream &file_handle )
{
    if( !file_handle ) {
        std::cerr << "Read Error: File cannot be accessed.";
        return true;
    }
    return false;
}

#ifndef QUIZ_PERSISTENCEMGR_H_
#define QUIZ_PERSISTENCEMGR_H_

#include "Quiz_Application.h"

struct PersistenceMgr
{
    void output( std::ofstream& file_handle, const QuizApplication &app );
    QuizApplication input( std::ifstream& file_handle );
};

#endif

#include "PersistenceMgr.h"

#include "Exception.h"

void PersistenceMgr::output( std::ofstream& file_handle, const QuizApplication& app )
{
    Exception e;
    if( e.ofile_handle_error( file_handle ) )
        return;

    boost::archive::text_oarchive archive{ file_handle };
    archive << app;
}

QuizApplication PersistenceMgr::input( std::ifstream& file_handle )
{
    Exception e;
    QuizApplication app{};
    if( e.ifile_handle_error( file_handle ) )
        return app; 
    
    boost::archive::text_iarchive archive{ file_handle };
    archive >> app;
    
    return app;
}

#ifndef QUIZ_QUIZAPPLICATION_H_
#define QUIZ_QUIZAPPLICATION_H_

#include "Quiz_Maker.h"

#include <vector>

#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/vector.hpp>

using namespace QuizMaker;
using QuizContainer = std::vector<QuizMaker::Quiz>;

class QuizApplication
{
    public:
        QuizApplication() = default;
        QuizApplication( const QuizContainer &quiz_c )
            : quiz_container_( quiz_c ) {}
        QuizApplication( std::ifstream &file_handle );
        void display();
        QuizApplication& append( const QuizMaker::Quiz &quiz );
        QuizApplication& remove( const std::string &title  );
        QuizContainer::const_iterator get( const std::string &title ) const;
        QuizContainer::iterator get( const std::string &title );
        unsigned count() const { return quiz_container_.size(); }
    private:
        QuizContainer quiz_container_;

        std::vector<std::string> tokenize( const std::string &line );
        TagContainer process_tag( const std::vector<std::string> &tokens );
        Question process_question( const std::vector<std::string> &tokens );
        void process_question_title( Question &question, const std::vector<std::string> &tokens );
        QuestionOptions process_options( const std::vector<std::string> &tokens );
        void process_correct_options( QuestionOptions &question_options, const std::vector<std::string> &tokens );

        friend class boost::serialization::access;
        template<typename Archive>
        void serialize( Archive &archive, const unsigned int version )
        {
            archive & quiz_container_;
        }
};

#endif

#include "Quiz_Application.h"

#include "Exception.h"

#include <vector>

using namespace QuizMaker;

QuizApplication::QuizApplication( std::ifstream &file_handle ) 
{
    Exception e;
    if( !e.ifile_handle_error( file_handle ) ) {

        std::string quiz_text;
        std::string word;

        while( !file_handle.eof() )
        {   
            file_handle >> word;
            if( word == "<quiz>" ) {
                continue; 
            }
            if( word == "</quiz>" )
            {
                QuizMaker::Quiz quiz_obj{};
                std::vector<std::string> tokens = tokenize( quiz_text );
                quiz_obj.quiz_tag_ = process_tag( tokens );
                quiz_obj.quiz_ = process_question( tokens );
                quiz_obj.quiz_options_ = process_options( tokens );
                tokens.clear();
                quiz_text = "";
                quiz_container_.push_back( quiz_obj );
                continue;   
            }
            quiz_text += word += " ";
        }   
    }
}

std::vector<std::string> QuizApplication::tokenize( const std::string &line )
{
    std::vector<std::string> tokens;
    std::stringstream stream( line );
    std::string intermediate;
    while( std::getline( stream, intermediate, ' ' ) )
        tokens.push_back( intermediate );

    return tokens; 
}

TagContainer QuizApplication::process_tag( const std::vector<std::string> &tokens )
{
    TagContainer tag;

    auto iter = std::find( tokens.cbegin(), tokens.cend(), "<tag>" );
    if( iter == tokens.cend() )
        return tag;

    auto iter2 = std::find( iter + 1, tokens.cend(), "</tag>" );
    if( iter2 == tokens.cend() )
        return tag;

    for( auto it = iter + 1; it != iter2; ++it )
        tag.tag_container_.push_back( *it );

    return tag;
}   

Question QuizApplication::process_question( const std::vector<std::string> &tokens )
{
    Question question_obj;
    auto iter = std::find( tokens.cbegin(), tokens.cend(), "<question>" );
    if( iter == tokens.cend() )
        return question_obj;

    auto iter2 = std::find( iter + 1, tokens.cend(), "</question>" );
    if( iter2 == tokens.cend() )
        return question_obj;

    for( auto it = iter + 1; it != iter2; ++it )
    {
        question_obj.question_ += *it;
        question_obj.question_ += " ";
    }

    process_question_title( question_obj, tokens );

    return question_obj;
}   

void QuizApplication::process_question_title( Question &question_obj, const std::vector<std::string> &tokens )
{
    auto iter = std::find( tokens.cbegin(), tokens.cend(), "<title>" );
    if( iter == tokens.cend() )
        return;

    auto iter2 = std::find( iter + 1, tokens.cend(), "</title>" );
    if( iter2 == tokens.cend() )
        return;

    for( auto it = iter + 1; it != iter2; ++it )
    {
        question_obj.title_ += *it;
        if( it != iter2 - 1 )
            question_obj.title_ += " ";
    }
}

QuestionOptions QuizApplication::process_options( const std::vector<std::string> &tokens )
{
    QuestionOptions question_options;

    auto iter = std::find( tokens.cbegin(), tokens.cend(), "<option>" );
    if( iter == tokens.cend() )
        return question_options;

    auto iter2 = std::find( tokens.cbegin(), tokens.cend(), "</option>" ); 
    if( iter2 == tokens.cend() )
        return question_options;

    auto iter_sub1 = std::find( iter + 1, iter2, "<option-item>" );
    while( iter_sub1 != iter2 ) 
    {   
        auto iter_sub2 = std::find( iter_sub1, iter2, "</option-item>" );
        if( iter_sub2 == iter2 )
            return question_options;

        std::string option;
        for( auto it = iter_sub1 + 1; it != iter_sub2; ++it )
        {
            option += *it;
            option += " ";
        }
        question_options.options_.push_back( option );
        iter_sub1 = std::find( iter_sub2, iter2, "<option-item>" );
    }

    process_correct_options( question_options, tokens );

    return question_options;
}

void QuizApplication::process_correct_options( QuestionOptions &question_options, const std::vector<std::string> &tokens )
{
    auto iter = std::find( tokens.cbegin(), tokens.cend(), "<correct-option>" );
    if( iter == tokens.cend() )
        return;

    auto iter2 = std::find( tokens.cbegin(), tokens.cend(), "</correct-option>" ); 
    if( iter2 == tokens.cend() )
        return;

   question_options.correct_option_ = *(iter + 1);
}

void QuizApplication::display()
{
    for( const auto item : quiz_container_ )
        std::cout << item.quiz_tag_ << "n" << item.quiz_ << "n" << item.quiz_options_ << "n";
}

QuizApplication& QuizApplication::append( const QuizMaker::Quiz &quiz ) 
{ 
    quiz_container_.push_back( quiz ); 
    
    return *this;
}
QuizApplication& QuizApplication::remove( const std::string &title  )
{
    auto iter = get( title );
    quiz_container_.erase( iter );

    return *this;
}
QuizContainer::const_iterator QuizApplication::get( const std::string &title ) const
{
    for( auto beg = quiz_container_.cbegin(); beg != quiz_container_.cend(); ++beg )
    {
        if( beg->quiz_.title_ == title )
            return beg;
    }
    Exception e;
    e.throw_get_error();
}
        
QuizContainer::iterator QuizApplication::get( const std::string &title )
{
    for( auto beg = quiz_container_.begin(); beg != quiz_container_.end(); ++beg )
    {
        if( beg->quiz_.title_ == title )
            return beg;
    } 
    Exception e;
    e.throw_get_error();
}

#include "Quiz_Application.h"
#include "PersistenceMgr.h"
#include "Quiz_Maker.h"

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

using namespace QuizMaker;

int main()
{
    std::ifstream file_handle{ "file.txt" };
    QuizApplication quiz_app{ file_handle };
    Question new_question{ "STL Containers", "The _______ container member returns true if no element is present otherwise returns false" };
    QuestionOptions my_options;
    my_options.options_ = {
        "emplace",
        "fill",
        "empty",
        "size"
    };
    my_options.correct_option_ = { "C" };
    TagContainer my_tags;
    std::vector<std::string> tags = { "STL", "Programming", "Algorithm" };
    my_tags.tag_container_ =  tags;

    Quiz new_quiz{ new_question, my_options, my_tags };
    quiz_app.append( new_quiz );


    /* delete a quiz */
    quiz_app.remove( "STL Containers" );

    /* modify a quiz */
    auto iter = quiz_app.get( "STL Equality Comparison" );
    iter->quiz_tag_.tag_container_.push_back( "STL" ); 

    quiz_app.display();

    /* Save to file */
    PersistenceMgr per_mgr;
    std::ofstream file_handle2{ "app.txt" };
    per_mgr.output( file_handle2, quiz_app );
    std::cout << "Quit total count: " << quiz_app.count() << "n";

    /* Load Quiz App */
    std::ifstream file_handle3{ "app.txt" };
    QuizApplication quiz_loaded = per_mgr.input( file_handle3 );
    std::cout << "Successfully loaded the applicationnn";
    quiz_loaded.display();
}

This is a sample question text file

<quiz>
    <tag> C++ Programming </tag>
    <title> STL Sorting Iterator </title>
    <question> The sort algorithm requires a(n) _____________ iterator </question>
    <option>
        <option-item> random-access </option-item>
        <option-item> bidirectional </option-item>
        <option-item> forward </option-item>
        <option-item> None of the above </option-item>
    </option>
    <correct-option> A </correct-option>
</quiz>

<quiz>
    <tag> C++ Programming </tag>
    <title> STL Equality Comparison </title>
    <question> The ___________ algorithm compares two sequence for equality </question>
    <option>
        <option-item> find </option-item>
        <option-item> equal </option-item>
        <option-item> fill_n </option-item>
        <option-item> copy </option-item>
    </option>
    <correct-option> B </correct-option>
</quiz>

<quiz>
    <tag> C++ Programming </tag>
    <title> STL Remove_if Algorithm </title>
    <question> The remove_if algorithm does not modify the number of elements in the container, 
    but it does move to the beginning of the containe all element that are not remove 
    </question>
    <option>
        <option-item> True </option-item>
        <option-item> False </option-item>
        <option-item> Not exactly </option-item>
        <option-item> Maybe </option-item>
    </option>
    <correct-option> A </correct-option>
</quiz>