Map with struct as key and vector as value

Hello,
Trying to challenge myself with C++ STL.
I want to read in data from file and do some calculation, adapted from an exercise of a book (C++ for engineers and scientist, 3rd Ed, Gary Bronson).
infile is like:

ID Name Course Credit Grade
2333021 Bokow,R. NS201 3 A
2333021 Bokow,R. MG342 3 A
2333021 Bokow,R. FA302 1 A
2574063 Failin,D. MK106 3 C
2574063 Failin,D. MA208 3 B
2574063 Failin,D. CM201 3 C
2574063 Failin,D. CP101 2 B
2663628 Kingsley,M. QA140 3 A
2663628 Kingsley,M. CM245 3 B
2663628 Kingsley,M. EQ521 3 A
2663628 Kingsley,M. MK341 3 A
2663628 Kingsley,M. CP101 3 B

My idea is to read in record as map for which ID and Name is the key, and courses as value. As the number of courses are different for each student, create two structures, one for student ID and name; the other for each course; push all the courses of the same student into a vector of structure. So the final format is like

map < struct IDnName; vector < struct Course> record> Record

My coding flow is:
1) getline of each row; parse it into tokens ;
2) assign tokens into struct IDnName and struct Course ;
3) push the two structs into map , struct IDnName as key and struct Course as value ;
4) push struct Course into vector if IDnName already exists;
Challenges to me:
1) Is this the right algorithm to handle the situation? If not, which is the right way?
2) For learning purpose, with my coding flow to print the record of each student, I break down the job into two functions, a) key_print() b) val_print() ; but I am not sure the parameters should be passed by value or reference, which I always have problem with.
I've attached my code, for which there were too many errors to be listed here when I tried to compile. Hope someone could help me out. Thanks a lot!

You don't use ; in templates, you use ,

map <type1, type2> name;

The "correct" way to do it really depends on how you want to do it, but I'd just boil it down to the bare minumum:

struct grades {
        string course, credit, grade;
};

map <int, vector < grades > > student_marks;
map <int, string> student_names;

This avoids the problem of using a custom struct/class as the "key" type for a map. You have to define > < = comparison overloads for that to work.

Pure integer keys are much faster anyway.

1 Like

Then comes to a new thing if two maps are used------they need share the key. While student_names only contains 3 unique entries(structure), student_courses contains 12 entries (structure) that are pushed into three vectors each for a student_names.
Do you mean to assign a new integer to both map?
The next step is to calculate the total credit of courses for each student; calculate the average grades for each student on his/her courses taken so far. So that I though use the string(or student ID) as key for retrieval and calculation.
The compiling error indicated the problem is the map iterator, which seems should be overloaded.

error: no match for 'operator=' in map_itr=mymap.std::map<_Key, _Tp, _Compare, _Alloc>::begin....
candidate is:
In file included from /usr/include/c++/4.7/map:60:0,
......

This challenge is too big for me at this moment. Thanks anyway!

Any challenge is too big when you take the path of maximum resistance.

This problem is exceptionally kind in giving you a unique, integer ID for each student... That's a gift to be embraced.

The code I gave you wasn't pseudocode. This is how I use it:

#include <stdio.h>

#include <map>
#include <vector>
#include <string>

using namespace std;

struct grades {
        string course, credit, grade;
};

int main(void)
{

        map<int, vector < grades > > marks;
        map<int, string> names;

        {
                int id=1234;
                const char *name="larry";
                grades grade={"a","b","c"};

                names[id]=name;
                marks[id].push_back(grade);
        }
}

If you wanted to bundle the data together and use it in a 'smart' way instead of updating individual variables, you could put it in a class with multiple members inside it. It still clearly belongs in multiple structures. How many times are you supposed to store someone's name and unique ID?

If you really wanted you could put the name and unique id in a pair<int, string>, which you should be able to plug into map < pair< int, string > , ... >

One of my original purposes of this practice is to experience more on vector and map, especially using a struct as key and vector for value in the map, so that I really did not mean to take the mximum resistance path . I like the slang "There are always more than one way to do it", and I tend to over-think many problems before I get to "take the minimum resistance path", but my always problem is how to find out the minimum resistance path.
Of course each unique student ID should be stored only once but not the student name, cases are students with same name but different ID number.
Thanks anyway and I will try using your method first then go back to the map<struct, vector<struct>> method, only for practice purpose to make a running code. Thanks a lot again!

So?

names[1234]="Joe";
names[1235]="Joe";

Two students, same name, zero problems.

To make your structure usable in a map, you should define an 'operator <' for it.

class asdf {
public:
        bool operator <(const asdf &rhs) const {
                // Return true if this class is less than RHS,
                // false otherwise
        }
};

How to do so can be fiendishly complex for complicated data, since you first need a coherent definition of what 'less' even means when comparing two student class records.

To make your structure usable in a map, you should define an 'operator <' for it.
How to do so can be fiendishly complex for complicated data, since you first need a coherent definition of what 'less' even means when comparing two student class records.
Definitely I under-estimated the challenge.
Did not realize this only after another two hours of googling/reading. In my original post there were too many errors to be listed here when I tried to compile. which really discouraged me! (Better now.)
Thanks a lot!

---------- Post updated at 05:41 PM ---------- Previous update was at 02:26 PM ----------

Kind of got lost after discussions and searching.
Originally intended output is like:

---------------------
2333021 Bokow,R. 
                 NS201 3 A
                 MG342 3 A
                 FA302 1 A 
2574063 Failin,D. 
                 MK106 3 C
                 MA208 3 B
                 CP101 2 B
2663628 Kingsley,M. 
                 QA140 3 A 
                 EQ521 3 A
                 MK341 3 A
                 CP101 3 B

So
1) I want student_ID be the key for the courses and names.
2) The records are read from a fstream which contains hundreds of thousands rows, imagine from a university record.
3) Also the student_ID may NOT be clustered together one row after another, so that read in record from file need to be checked for each entry before it is pushed into map.

1) OK. The data structures I gave you do exactly this.

2) OK.

3) OK.

So...

What is your question, exactly?

Mainly technical problems:
One difference from your code is the the vector part which I thought is a structure containing course_abbreviation, credit and grade. Yours is a vector of string. Which is the part I did not catch with your code?
code:

#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <map>
#include <vector>
#include <cstdlib>

using namespace std;
struct grades {
    string course_Abrrev; 
    int course_Credit; 
    char course_Grade;};

struct IDnName { 
    int student_ID; 
    string student_Name;};

int main(int argc, char *argv[])
{
    vector <struct grades> Courses;
    map < string, vector < grades > >marks;
    map < string, string > names;

 ifstream inFILE;
    inFILE.open(argv[1]);

    if (inFILE.fail()) {
    cout << "ERROR: File failed to open!" << endl;
    } else {
    string line;                //string for one line
    struct IDnName oneName;        // Allocate a place for one ID and name
    struct grades oneCourse;    // Allocate a place for a structure for a record of one course

    while (inFILE.good()) {
        char *sPtr;                //string pointer for token
        char *sArray = new char[line.length() + 1];    //Initialize pointer sArray for string--->char* conversion

        getline(inFILE, line);    //Read the whole line
        strcpy(sArray, line.c_str());
        //tokenize the line into array
        sPtr = strtok(sArray, " ");    
//        oneName.student_ID = atoi(sPtr[0].c_str());   // did not work
        oneName.student_ID = stoi(sPtr);                                //c++ method

        while (sPtr != NULL) {
        oneName.student_Name = sPtr;
        names.insert(make_pair(oneName.student_ID, oneName.student_Name));        //LINE 54
        sPtr = strtok(NULL, " ");
        oneCourse.course_Abrrev = sPtr;
        sPtr = strtok(NULL, " ");
//        oneCourse.course_Credit = atoi(sPtr[0].c_str());  // did not work
        oneCourse.course_Credit = stoi(sPtr);
        sPtr = strtok(NULL, " ");
        oneCourse.course_Grade = sPtr[0];
        }

        if (marks.find(oneName.student_ID) != marks.end()) {                //LINE 65
        marks.insert(make_pair(oneName.student_ID, vector<struct grades>()));        //LINE 66
        } else {
        Courses.push_back(oneCourse);
        }
    }
    }

// print part not ready yet
return 0;
}

Highlighted LINE 54, 65 & 66 are the parts throwing out lots of errors. Thanks!

That wasn't what I meant by using pairs, but I think you'd do map.insert( std::pair<type,type>(valuea, valueb) )

The reason your 'atoi' didn't work is because you're trying to call c_str on something that's already a c string! atoi(sptr); stoi is just an alias to atoi on systems that have it(unlike mine, which did not.) Use atoi.

That 'courses' structure doesn't look useful for anything, not sure why you have it, all the information is in 'marks'.

Finally -- I think you missed the point. Imagine how they make these map<>'s, it's going to be a list or array of some kind internally, right? So in memory somewhere there is a structure like this:

struct map_element {
        struct
        {
                string studentid;
        } key;

        struct {
                int studentid;
                string payload;
        } value;
};

This isn't just wasted memory, it creates the opportunity for your program to store "impossible" states where element.key.studentid and element.value.studentid disagree. This seems even more likely to happen when keeping the same value around in different places as different types!

So:

  1. Use studentid as an integer everywhere.
  2. Never duplicate your key inside your data, only use it to fetch your data.

Working on your program.

Thanks!
Courses is a vector of struct, which seems needed as (map) value of marks. It is to hold the grades structures when more courses are pushed into the vector until eof. So the vector sizes are very likely different for each student. In the example input file, there are 3 students that have 3, 4 and 5 courses, respectively.
like:

pair1 <2333021, Courses(size=3)>
pair2 <2475063, Courses(size=4)>
pair3 <2663628, Courses(size=5)>
......

Did I miss anything?

Your program is a mess, had no error checking and crashed nonstop. That's why strtok is considered "evil" -- if you don't bother checking, bad things will happen. But a little checking makes it fine.

Nearly every line ended up commented out and replaced, I had to rewrite it instead.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>     // You need this for atoi, see 'man atoi'

#include <map>
#include <vector>
#include <string>
using namespace std;

struct grades {
    string course_Abrrev;
    int course_Credit;
    char course_Grade;
};

int main(void)
{
        char buf[4096];
        map<int, string> names;
        map<int, vector < grades > > marks;

        // Read a line into 'buf'
        while(fgets(buf, 4096, stdin) != NULL)
        {
                // Array of 10 pointers.
                // Each will point to a position inside 'buf', or
                // just point to "".
                const char *token[10];
                grades grade;
                int n=0;

                token[n]=strtok(buf, " \r\n\t");
                while(n < 10)
                {
                        n++;
                        token[n]=strtok(NULL, " \r\n\t");
                        if(token[n]==NULL) break;
                }
                // Set all NULL strings to "" instead.
                while(n < 10) token[n++]="";

                // There, now we have a nice array of tokens
                // which are all guaranteed not to crash when used.
                // The very worst any of them will be is blank...

                n=atoi(token[0]);       // Get student ID

                grade.course_Abrrev=token[2];
                grade.course_Credit=atoi(token[3]);
                grade.course_Grade=*(token[3]);

                names[n]=token[1];              // Store name
                marks[n].push_back(grade);      // store grade
        }

        for(map<int, string>::iterator i=names.begin();
                i != names.end();
                i++)
        {
                int id=(*i).first;
                printf("%d %s\n", id, (*i).second.c_str());

                for(vector<grades>::iterator n=marks[id].begin();
                        n != marks[id].end();
                        n ++ )
                {
                        printf("\t%s %d %c\n",
                                (*n).course_Abrrev.c_str(),
                                (*n).course_Credit,
                                (*n).course_Grade);
                }

        }
}
1 Like

Why? A vector can hold as many as you want already, and if you want to know how large it is, just ask: (something).size()

Logically I was thinking in a totally different way! Definitely I missed many points that may be critical for programming. Let me try out your code first. Thanks a lot!

Simpler version using more C functions:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>     // You need this for atoi

#include <map>
#include <vector>
#include <string>
using namespace std;

struct course {
    char abbrev[11];
    int credit;
    char grade;
};

int main(void)
{
        char buf[4096];
        map<int, string> names;
        map<int, vector < course > > marks;

        // Read a line into 'buf'
        while(fgets(buf, 4096, stdin) != NULL)
        {
                course grade;
                char name[21];
                int id;

                if(sscanf(buf, "%d %20s %10s %d %c",
                        &id,name,grade.abbrev,&grade.credit,&grade.grade) != 5)
                {
                        fprintf(stderr, "Bad line %s\n", buf);
                        continue;
                }

                names[id]=name;
                marks[id].push_back(grade);
        }

        for(map<int, string>::iterator i=names.begin();
                i != names.end();
                i++)
        {
                int id=(*i).first;
                printf("%d %s\n", id, (*i).second.c_str());

                for(vector<course>::iterator n=marks[id].begin();
                        n != marks[id].end();
                        n ++ )
                {
                        printf("\t%s %d %c\n",
                                (*n).abbrev, (*n).credit, (*n).grade);
                }
        }
}

sscanf shortens it by 20 lines, you can just tell it "lines should like this" and it'll sort it into the arguments you asked for and tell you whether it could. It even protects against overflows if you tell it to (%20s, so it doesn't overrun a 21-char buffer).

This is what bugs me most about the 'scanf is dangerous, strtok is dangerous' camp. As opposed to what, hand-writing your own unchecked ad-hoc string parser? At least C has a parser.

1 Like

It even protects against overflows if you tell it to (%20s, so it doesn't overrun a 21-char buffer).
Those are only concerns of advanced programmers. I can't predict the situations even you pointed it out.
BTW, the last version has a bug if there is a blank line in the input file....(No worry, I'll try to fix it myself.)
The biggest point for me is from the lines:

names[id] = name;
marks[id].push_back(grade);

I did not know this way to assign a map_pair so much simply.

Thanks a lot!

A crashed program doesn't do the job whether you're beginner or advanced.

If you don't start thinking about it when you're a beginner, you never will.

No it doesn't:

$ cat student:

2333021 Bokow,R. NS201 3 A
2333021 Bokow,R. MG342 3 A
2333021 Bokow,R. FA302 1 A
2574063 Failin,D. MK106 3 C
2574063 Failin,D. MA208 3 B
2574063 Failin,D. CM201 3 C
2574063 Failin,D. CP101 2 B
2663628 Kingsley,M. QA140 3 A
2663628 Kingsley,M. CM245 3 B
2663628 Kingsley,M. EQ521 3 A
2663628 Kingsley,M. MK341 3 A
2663628 Kingsley,M. CP101 3 B
$ ./a.out

Bad line

2333021 Bokow,R.
        NS201 3 A
        MG342 3 A
        FA302 1 A
2574063 Failin,D.
        MK106 3 C
        MA208 3 B
        CM201 3 C
        CP101 2 B
2663628 Kingsley,M.
        QA140 3 A
        CM245 3 B
        EQ521 3 A
        MK341 3 A
        CP101 3 B

$
1 Like

BTW, the last version has a bug if there is a blank line in the input file.... I meant your first version.
No worry at all!
How much do I owe you?!

Nothing, I post here for free.

2 Likes

Posted by Corona688:

Really Like you Corona688 :b:. You are really ideal for people like me and others who want to learn technology.

Thanks,
R. Singh

1 Like