3
\$\begingroup\$

For anyone who's unfamiliar with this grading system.

I'd like a general review of my code, and I have a few questions:

  1. Are plain types okay for the letter grades and credit hours, or could they be public typedefs?
  2. Should operator<< need to call calculateGPA(), or should I create a private accessor and have that called? In order to maintain encapsulation, I'll need to keep operator<< as a friend.
  3. Is the for-loop in calculateGPA() ideal for this purpose, or are there one or more STL functions that can do the same thing but more cleanly?

GPA.h

#ifndef GPA_H
#define GPA_H

#include <iosfwd>
#include <vector>
#include <utility>

class GPA
{
private:
    std::vector<std::pair<unsigned, unsigned> > grades;
    double calculateGPA() const;

public:
    void add(char, unsigned);
    friend std::ostream& operator<<(std::ostream&, GPA const&);
};

unsigned convertLetterGrade(char);

#endif

GPA.cpp

#include "GPA.h"
#include <ostream>
#include <stdexcept>

void GPA::add(char letterGrade, unsigned creditHours)
{
    unsigned numericalGrade = convertLetterGrade(letterGrade);
    grades.push_back(std::make_pair(numericalGrade, creditHours));
}

double GPA::calculateGPA() const
{
    unsigned sumGradePoints = 0;
    unsigned sumCreditHours = 0;

    for (auto iter = grades.cbegin(); iter != grades.cend(); ++iter)
    {
        sumGradePoints += iter->first * iter->second;
        sumCreditHours += iter->second;
    }

    double totalGPA = static_cast<double>(totalGradePoints) / sumCreditHours;
    return totalGPA;
}

unsigned convertLetterGrade(char letterGrade)
{
    switch (letterGrade)
    {
        case 'a':
        case 'A': return 4;
        case 'b':
        case 'B': return 3;
        case 'c':
        case 'C': return 2;
        case 'd':
        case 'D': return 1;
        case 'f':
        case 'F': return 0;
        default: throw std::logic_error("Invalid Letter Grade");
    }
}

std::ostream& operator<<(std::ostream& out, GPA const& obj)
{
    return out << obj.calculateGPA();
}

Main.cpp

#include "GPA.h"
#include <cstddef>
#include <iostream>

int main()
{
    std::cout << "# Classes: ";
    std::size_t numClasses;
    std::cin >> numClasses;

    GPA gpa;

    for (std::size_t i = 0; i < numClasses; ++i)
    {
        std::cout << "\n(" << i+1 << ") Letter Grade: ";
        char letterGrade;
        std::cin >> letterGrade;

        std::cout << "    Credit Hours: ";
        unsigned creditHours;
        std::cin >> creditHours;

        gpa.add(letterGrade, creditHours);
    }

    std::cout << "\n*** Total GPA: " << gpa;
}
\$\endgroup\$
4
  • \$\begingroup\$ with deciding to make letter grades and credit hours public, I would ask, what advantage is there to making them public? But what are the disadvantages, weighed against the limitations of having them private.. I would be inclined to keep them private.. if this doesn't make sense let me know cheers \$\endgroup\$
    – user90823
    Commented Aug 6, 2013 at 17:48
  • \$\begingroup\$ @Skippy: With typedefs, this is entirely different. They generally rename a type, but doesn't have much to do with encapsulation. They could help personalize my class a bit, plus the user obviously already knows about those two variables. They just don't know about the std::vector inside the class (and even if I were to typedef that, it would obviously be private). \$\endgroup\$
    – Jamal
    Commented Aug 6, 2013 at 17:54
  • \$\begingroup\$ hey thnx for that, I wasn't sure if I understood yr point properly, I appreciate yr patience (I am here to learn you see :)) \$\endgroup\$
    – user90823
    Commented Aug 6, 2013 at 17:57
  • 1
    \$\begingroup\$ @Skippy: That's why we're here. :-) \$\endgroup\$
    – Jamal
    Commented Aug 6, 2013 at 18:01

1 Answer 1

2
\$\begingroup\$

Note that I am completely unfamiliar with the american(?) grade system and GPAs -- I am reviewing the coding style and approach, not whether or not the solution is correct.

General notes:

  • Your GPA class is not really a class. It's actually just a function. It could just as well be implemented as double calculateGPA(std::vector<std::pair<char, unsigned>>). Beware of the golden hammer, especially when it comes to classes.

  • You can use std::tolower() to halve the size of your switch.

  • vector comes after utility in the alphabet :-)

  • C++11 allows writing >> in nested templates.

  • Your program won't compile: totalGradePoints is called sumGradePoints.

  • You can probably use float instead of double.

  • Make the local variables in calculateGPA() floating point numbers from the start, and drop the cast.

  • obj should be called gpa or something like that.

  • Write unit tests.


Are plain types okay for the letter grades and credit hours, or could they be public typedefs?

Either is fine. Using a typedef makes the intent of a variable a bit clearer. This added distinction between types could help the developer realizing he or she is making a mistake. However, a typedef is sadly just a type alias and does not create a new type! In other words, you do not get any type-safety benefits of doing so.

To get full type safety, you would need to create a (templated) wrapper class around the types in question. Doing so would be a nice exercise when you start to learn about templates.


Should operator<< need to call calculateGPA(), or should I create a private accessor and have that called? In order to maintain encapsulation, I'll need to keep operator<< as a friend.

I would cache the result of calculateGPA and provide an accessor to it. Have operator<< call that accessor. This means that your class would need to invalidate the cached value whenever grades is modified.


Is the for-loop in calculateGPA() ideal for this purpose, or are there one or more STL functions that can do the same thing but more cleanly?

The for loop is sufficiently short and sweet, and makes just a single pass (unlike most other solutions I can think of). You could always use std::accumulate and a lambda, but you would have to do so for both sumCreditPoints and sumGradeHours.

Am I guilty of converting you to the const& style? :-)

\$\endgroup\$
12
  • \$\begingroup\$ Yeah, the typedefs are more for aesthetics anyway. I have barely covered templates, but it doesn't seem to quite fit my intent here (I think). If I were to go with the first point, would there be no GPA class at all? I suppose that means everything would be in one file, then. Yes, I did eventually catch the includes, and I now prefer const& now (it reads more as "const-reference"). :-) Finally: I still cannot use a range-based for-loop for calculateGPA(). :/ \$\endgroup\$
    – Jamal
    Commented Aug 6, 2013 at 18:09
  • \$\begingroup\$ Is this what you mean by the "golden hammer"? \$\endgroup\$
    – Jamal
    Commented Aug 6, 2013 at 18:13
  • \$\begingroup\$ With a templated wrapper class you could do typedef unique_type_wrapper<unsigned> HourCount, and then HourCount would be a distinct type. | You would have no GPA class, but you could still put the function in a separate file (and make helper functions in that file static). | Yes, and classes are a typical golden hammer. Just look at Java :-) \$\endgroup\$
    – Lstor
    Commented Aug 6, 2013 at 18:16
  • \$\begingroup\$ unique_type_wrapper? Never heard of that before. Just when I thought my code was "C++-enough," there's even more C++ I could use. Would further discussion on this in chat be okay, even if it may involved writing code (off-topic)? \$\endgroup\$
    – Jamal
    Commented Aug 6, 2013 at 18:19
  • \$\begingroup\$ unique_type_wrapper is the class you write when you learn about templates to allow type-safe type aliases. \$\endgroup\$
    – Lstor
    Commented Aug 6, 2013 at 18:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.