user9589156
user9589156

Reputation:

Iterate over class members

Hi I want to know if I could iterate class members so I don't have to have writeElement for every class member.

I would love to have it in a for loop so it loops all the public members.

My code:

class Student
{
public:
    string name;
    string lastName;
    int age;
    string gender;
    vector<int> grades;

public:
    void read(istream& in)
    {
        readElement(in, name);
        readElement(in, lastName);
        readElement(in, age);
        readElement(in, gender);
        readElement(in, grades);
    }

    void write(ostream& out)
    {
        //add a loop here
        writeElement(out, name);
        writeElement(out, lastName);
        writeElement(out, age);
        writeElement(out, gender);
        writeElement(out, grades);
    }
};

Upvotes: 4

Views: 864

Answers (2)

Francis Cugler
Francis Cugler

Reputation: 7905

Since the feature you are looking for doesn't quite exist yet; maybe something of this nature will help you. This does currently compile and run as I have tested some of the basic functionality, but I have not done any exhaustive testing. I abstracted all of the student information out of the student class into member that is a tuple, with some overloaded operator()s. For this to work as is; I had to overload both the ostream & istream operators for both vectors and tuples for this to work.

#include <iostream>
#include <string>
#include <utility>
#include <tuple>
#include <vector>

// ostream operator<< for vector<T>
template<class T>
std::ostream& operator<<( std::ostream& out, const std::vector<T>& v ) {
    out << "{ ";
    for( auto& a : v )
        out << a << ' ';
    out << '}';
    return out;
}

// istream operator>> for vector<T>
template<class T>
std::istream& operator>>( std::istream& in, std::vector<T>& v ) {
    int i;
    std::string line;
    std::getline( std::cin, line );
    std::istringstream iss( line );
    while( iss >> i ) {
        v.push_back( i );
    }
    return in;
}

// function templates & ostream operator<< for tuple<T>
template<std::size_t> struct int_ {};

template<class Tuple, size_t Pos>
std::ostream& print_tuple( std::ostream& out, const Tuple& t, int_<Pos> ) {
    out << std::get<std::tuple_size<Tuple>::value - Pos>( t ) << ' ';
    return print_tuple( out, t, int_<Pos - 1>() );
}

template<class Tuple>
std::ostream& print_tuple( std::ostream& out, const Tuple& t, int_<1> ) {
    return out << std::get<std::tuple_size<Tuple>::value - 1>( t );
}

template<class... Args>
std::ostream& operator<<( std::ostream& out, const std::tuple<Args...>& t ) {
    return print_tuple( out, t, int_<sizeof...(Args)>() );
}

// function templates & istream operator << for tuple<T>
template<class Tuple, size_t Pos>
std::istream& write_tuple( std::istream& in, Tuple& t, int_<Pos> ) {
    in >> std::get<std::tuple_size<Tuple>::value - Pos>( t );
    return write_tuple( in, t, int_<Pos - 1>() );
}

template<class Tuple>
std::istream& write_tuple( std::istream& in, Tuple& t, int_<1> ) {
    return in >> std::get<std::tuple_size<Tuple>::value - 1>( t );
}

template<class... Args>
std::istream& operator>>( std::istream& in, std::tuple<Args...>& t ) {
    return write_tuple( in, t, int_<sizeof...(Args)>() );
}
// --------------------------------------------------

// class proto type for friend operators
template<class... T>
class StudentInfo;

template<class... T>
std::ostream& operator<< <>( std::ostream& out, const StudentInfo<T...>& c );

template<class... T>
std::istream& operator>> <>( std::istream& in, StudentInfo<T...>& c );

//template<typename... Args>
template<class...Args>
class StudentInfo {
public
    std::tuple<Args...> members;


    explicit StudentInfo(Args&&... args ) {
        members = std::make_tuple<Args...>( std::move( args )... );
    } 

    const StudentInfo<Args...>& operator() ( Args&&... args ) {
        members = std::make_tuple<Args...>( std::forward<Args>( args )... );
        return *this;
    }

    const StudentInfo<Args...> operator() ( Args&&... args ) const {
        members = std::make_tuple<Args...>( std::forward<Args>( args )... );
        return *this;
    }

    template<Args...>
    friend std::ostream& operator<< <>(std::ostream& out, const StudentInfo<Args...>& c);

    template<Args...>
    friend std::istream& operator>> <>( std::istream& in, StudentInfo<Args...>& c );

    StudentInfo<Args...>& operator=( StudentInfo<Args...>& c ) {
        if ( members == c.members ) 
            return *this;
        members = c.members;
        return *this;
    }

};

template<class... T>
std::ostream& operator<< <>( std::ostream& out, StudentInfo<T...>& c ) {
    return out << c.members;
}

template<class... T>
std::istream& operator>> <>( std::istream& in, StudentInfo<T...>& c ) {
    return in >> c.members;
}

Using it is as follows:

int main() {
    std::string first{ "Some" };
    std::string last{ "Day" };
    int age = 1000;
    std::string sex{ "Unknown" };
    std::vector<int> grades{ 99, 98, 97, 92, 89, 88 };

    // create student info
    StudentInfo< std::string, std::string, int,
                 std::string, std::vector<int> > 
        studentA( std::move(first), std::move(last), 
                  std::move(age), std::move(sex), 
                  std::move(grades)
        );

    // outstream student info
    std::cout << studentA << '\n';

    // reset temps
    first.clear();
    last.clear();
    age = 0;
    sex.clear();
    grades.clear();

    // create 2nd student & assign new information from user input
    StudentInfo< std::string, std::string, int,
                 std::string, std::vector<int> >
        studentB( std::move(first), std::move(last), 
                  std::move(age), std::move(sex), 
                  std::move(grades)
        );

    // Check to make sure it has empty fields
    std::cout << "Student B's information\n" << studentB << '\n';

    // Now let's enter some stuff from the console and populate studentB
    std::cout << "\nEnter the student's information\n";
    std::cin >> studentB;

    // Let's check studentB's info
    std::cout << studentB << '\n';

    // Another step let's check our assignment operator
    StudentInfo< std::string, std::string, int,
                 std::string, std::vector<int> >
        studentC( std::move( first ), std::move( last ),
                  std::move( age ), std::move( sex ),
                  std::move( grades ) 
        );

    // Check studentC it should be empty
    std::cout << "Student C's information\n" << studentC << '\n';

    // Let's set studentC to studentA;
    studentC = studentA;

    // Print C's info
    std::cout << "Student C's new information\n" << studentC << '\n';

    // Finally test out the operator()
    studentC( std::move( std::get<0>( studentB.members ) ),
              std::move( std::get<1>( studentB.members ) ),
              std::move( std::get<2>( studentB.members ) ),
              std::move( std::get<3>( studentB.members ) ),
              std::move( std::get<4>( studentB.members ) )
    );
    std::cout << studentC << '\n';


    std:cout << "\nPress any key and enter to quit.\n";
    std::cin.get();
    return 0;
}

The implementation looks complex, but some parts are needed when doing variadic class templates and working with tuples.

Just writing the overload and helper functions for tuple alone wasn't enough. It would work for tuples where all of its elements are of a basic type <T>. However when a tuple has a member that is not a basic type such as a container std::vector<T>. Then the code will break and it won't compile because it doesn't know how to hand the case of a vector. The code above will work as is; however if you try to substitute another container type for the vector the above iostream overloads for the tuple will again break as it only knows how to hand the vector type. You would have to manually add in the support for the other containers.

Upvotes: 0

Vittorio Romeo
Vittorio Romeo

Reputation: 93274

There is no easy way of achieving what you desire. Static reflection, proposed for C++20, will make this possible.

Right now, you have a few (not great) options:

  • Manually write out the readElement/writeElement calls as you did. You can avoid the repetition by providing an higher order function and passing readElement/writeElement as arguments (kinda like a visitor).

  • Wrap the entire struct definition into a variadic macro that automatically generates a visitor on every data member.

  • Use std::tuple instead of a struct, and use std::apply + variadic generic lambda to "iterate" over the members.

  • If your type supports it, you could use magic_get, which (ab)uses structured bindings and other crazy metaprogramming tricks to provide a limited form of static reflection.


Visitor solution example:

template <typename F>
void visit(F&& f)
{
    f(name);
    f(lastName);
    f(age);
    f(gender);
    f(grades);   
}

void read(istream& in)
{
    visit([&in](auto&& x){ readElement(in, x); });
}

void write(ostream& out)
{
    visit([&in](auto&& x){ writeElement(in, x); });
}

Tuple solution example:

std::tuple
<
    string      /* name */,
    string      /* lastName */,
    int         /* age */,
    string      /* gender */,
    vector<int> /* grades */
> data;

template <typename F>
void visit(F&& f)
{
    std::apply([](auto&&... xs){ (f(xs), ...); }, data);
}

Upvotes: 4

Related Questions