Date class implementation in C++

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
12
down vote

favorite
2












Here is my implementation of a Date class.



I am interested mainly in the class implementation and design, whether or not it has any major drawbacks and inefficiencies. But I will appreciate any given recommendations, warnings and advises.



The creation of the class is only for learning purposes, production usage is not intended.



Date.h:



#pragma once
#ifndef _DATE_H_
#define _DATE_H_

#include <iostream>
#include <string>

class Date
public:
struct BadDate;
enum Month
jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
;

private:
static std::string monthNames [12];
static bool defaultSet;
static Date defaultDate;

int _day, _month, _year;
bool leapYear(const int) const;
void fillDate(int d, Month m, int y);
void checkIllFormed();
bool isIllFormed() const;
void setVal(int&, const int, const int);

public:
static void setDefault(const int d = 1, Month = Month(1), const int y = 2000);
static void showDefault(std::ostream& os);
static const std::string monthNameByNumber(const int);

Date(
int d = (Date::defaultSet)?Date::defaultDate.day():1,
Month m = (Date::defaultSet)?Date::defaultDate.month():Month(1),
int y = (Date::defaultSet)?Date::defaultDate.year():2000
);
Date(int d, int m, int y);
Date(const Date&);
Date& operator=(const Date&);
~Date();

int day() const;
Month month() const;
int year() const;

const std::string getMonthName() const;

void setDay(const int);
void setMonth(const int);
void setYear(const int);

const Date& operator++();
const Date operator++(int);
const Date& operator--();
const Date operator--(int);
;

struct Date::BadDate
int _day, _month, _year;
BadDate(int d, int m, int y);
;

std::ostream& operator<<(std::ostream&, const Date&);
std::ostream& operator<<(std::ostream&, const Date::BadDate&);

#endif


Date.cpp:



#include "Date.h"

Date::BadDate::BadDate(int d, int m, int y)
: _day(d), _month(m), _year(y)

;

std::string Date::monthNames[12] =
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
;
bool Date::defaultSet = true;
Date Date::defaultDate = Date(1, 1, 2000);

void Date::setDefault(const int d, Month m, const int y)
Date::defaultDate.setDay(d);
Date::defaultDate.setMonth(m);
Date::defaultDate.setYear(y);
Date::defaultSet = true;

void Date::showDefault(std::ostream& os)
os << Date::defaultDate;

const std::string Date::monthNameByNumber(const int n)
return Date::monthNames[n-1];


Date::Date(int d, Month m, int y)
: _day(d), _month(m), _year(y)
checkIllFormed();

Date::Date(int d, int m, int y)
: _day(d), _month(Month(m)), _year(y)
checkIllFormed();

Date::Date(const Date& that)
: _day(that.day()), _month(that.month()), _year(that.year())
checkIllFormed();

void Date::checkIllFormed()
if(isIllFormed())
BadDate bd = BadDate(day(), month(), year());
*this = Date::defaultDate;
throw bd;


Date& Date::operator=(const Date& that)
fillDate(that.day(), that.month(), that.year());
return *this;

Date::~Date(void)



int Date::day() const
return _day;

Date::Month Date::month() const
return Month(_month);

int Date::year() const
return _year;


const std::string Date::getMonthName() const
return Date::monthNameByNumber(month());


void Date::setDay(const int d)
setVal(_day, d, day());

void Date::setMonth(const int m)
setVal(_month, m, month());

void Date::setYear(const int y)
setVal(_year, y, year());

void Date::setVal(int& val, const int newVal, const int prevVal)
val = newVal;
if(isIllFormed())
BadDate bd = BadDate(day(), month(), year());
val = prevVal;
throw bd;



const Date& Date::operator++()
setDay(day() + 1);
return *this;

const Date Date::operator++(int)
setDay(day() + 1);
return *this;

const Date& Date::operator--()
setDay(day() - 1);
return *this;

const Date Date::operator--(int)
setDay(day() - 1);
return *this;


std::ostream& operator<<(std::ostream& os, const Date& d)
os << d.day() << '.' << d.getMonthName() << '.' << d.year();
return os;

std::ostream& operator<<(std::ostream& os, const Date::BadDate& bd)
os << bd._day << '.' << Date::monthNameByNumber(bd._month) << '.' << bd._year;
return os;


bool Date::leapYear(const int y) const
/*
1.If the year is evenly divisible by 4, go to step 2. Otherwise, go to step 5.
2.If the year is evenly divisible by 100, go to step 3. Otherwise, go to step 4.
3.If the year is evenly divisible by 400, go to step 4. Otherwise, go to step 5.
4.The year is a leap year (it has 366 days).
5.The year is not a leap year (it has 365 days).
*/
if(y%4)
return false;

if(y%100)
return true;

if(y%400)
return false;

return true;

void Date::fillDate(int d, Month m, int y)
setDay(d);
setMonth(m);
setYear(y);


bool Date::isIllFormed() const


I saw this other question, but my Date class is different and may have its own pitfalls which I am not aware of and I would like to know.







share|improve this question





















  • Have you implemented this only as an exercise? If not, why not try Boost DateTime?
    – einpoklum
    Feb 25 at 11:30










  • You might also like to take a look at Howard Hinnant's date library which is based on std::chrono.
    – user673679
    Feb 25 at 16:16

















up vote
12
down vote

favorite
2












Here is my implementation of a Date class.



I am interested mainly in the class implementation and design, whether or not it has any major drawbacks and inefficiencies. But I will appreciate any given recommendations, warnings and advises.



The creation of the class is only for learning purposes, production usage is not intended.



Date.h:



#pragma once
#ifndef _DATE_H_
#define _DATE_H_

#include <iostream>
#include <string>

class Date
public:
struct BadDate;
enum Month
jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
;

private:
static std::string monthNames [12];
static bool defaultSet;
static Date defaultDate;

int _day, _month, _year;
bool leapYear(const int) const;
void fillDate(int d, Month m, int y);
void checkIllFormed();
bool isIllFormed() const;
void setVal(int&, const int, const int);

public:
static void setDefault(const int d = 1, Month = Month(1), const int y = 2000);
static void showDefault(std::ostream& os);
static const std::string monthNameByNumber(const int);

Date(
int d = (Date::defaultSet)?Date::defaultDate.day():1,
Month m = (Date::defaultSet)?Date::defaultDate.month():Month(1),
int y = (Date::defaultSet)?Date::defaultDate.year():2000
);
Date(int d, int m, int y);
Date(const Date&);
Date& operator=(const Date&);
~Date();

int day() const;
Month month() const;
int year() const;

const std::string getMonthName() const;

void setDay(const int);
void setMonth(const int);
void setYear(const int);

const Date& operator++();
const Date operator++(int);
const Date& operator--();
const Date operator--(int);
;

struct Date::BadDate
int _day, _month, _year;
BadDate(int d, int m, int y);
;

std::ostream& operator<<(std::ostream&, const Date&);
std::ostream& operator<<(std::ostream&, const Date::BadDate&);

#endif


Date.cpp:



#include "Date.h"

Date::BadDate::BadDate(int d, int m, int y)
: _day(d), _month(m), _year(y)

;

std::string Date::monthNames[12] =
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
;
bool Date::defaultSet = true;
Date Date::defaultDate = Date(1, 1, 2000);

void Date::setDefault(const int d, Month m, const int y)
Date::defaultDate.setDay(d);
Date::defaultDate.setMonth(m);
Date::defaultDate.setYear(y);
Date::defaultSet = true;

void Date::showDefault(std::ostream& os)
os << Date::defaultDate;

const std::string Date::monthNameByNumber(const int n)
return Date::monthNames[n-1];


Date::Date(int d, Month m, int y)
: _day(d), _month(m), _year(y)
checkIllFormed();

Date::Date(int d, int m, int y)
: _day(d), _month(Month(m)), _year(y)
checkIllFormed();

Date::Date(const Date& that)
: _day(that.day()), _month(that.month()), _year(that.year())
checkIllFormed();

void Date::checkIllFormed()
if(isIllFormed())
BadDate bd = BadDate(day(), month(), year());
*this = Date::defaultDate;
throw bd;


Date& Date::operator=(const Date& that)
fillDate(that.day(), that.month(), that.year());
return *this;

Date::~Date(void)



int Date::day() const
return _day;

Date::Month Date::month() const
return Month(_month);

int Date::year() const
return _year;


const std::string Date::getMonthName() const
return Date::monthNameByNumber(month());


void Date::setDay(const int d)
setVal(_day, d, day());

void Date::setMonth(const int m)
setVal(_month, m, month());

void Date::setYear(const int y)
setVal(_year, y, year());

void Date::setVal(int& val, const int newVal, const int prevVal)
val = newVal;
if(isIllFormed())
BadDate bd = BadDate(day(), month(), year());
val = prevVal;
throw bd;



const Date& Date::operator++()
setDay(day() + 1);
return *this;

const Date Date::operator++(int)
setDay(day() + 1);
return *this;

const Date& Date::operator--()
setDay(day() - 1);
return *this;

const Date Date::operator--(int)
setDay(day() - 1);
return *this;


std::ostream& operator<<(std::ostream& os, const Date& d)
os << d.day() << '.' << d.getMonthName() << '.' << d.year();
return os;

std::ostream& operator<<(std::ostream& os, const Date::BadDate& bd)
os << bd._day << '.' << Date::monthNameByNumber(bd._month) << '.' << bd._year;
return os;


bool Date::leapYear(const int y) const
/*
1.If the year is evenly divisible by 4, go to step 2. Otherwise, go to step 5.
2.If the year is evenly divisible by 100, go to step 3. Otherwise, go to step 4.
3.If the year is evenly divisible by 400, go to step 4. Otherwise, go to step 5.
4.The year is a leap year (it has 366 days).
5.The year is not a leap year (it has 365 days).
*/
if(y%4)
return false;

if(y%100)
return true;

if(y%400)
return false;

return true;

void Date::fillDate(int d, Month m, int y)
setDay(d);
setMonth(m);
setYear(y);


bool Date::isIllFormed() const


I saw this other question, but my Date class is different and may have its own pitfalls which I am not aware of and I would like to know.







share|improve this question





















  • Have you implemented this only as an exercise? If not, why not try Boost DateTime?
    – einpoklum
    Feb 25 at 11:30










  • You might also like to take a look at Howard Hinnant's date library which is based on std::chrono.
    – user673679
    Feb 25 at 16:16













up vote
12
down vote

favorite
2









up vote
12
down vote

favorite
2






2





Here is my implementation of a Date class.



I am interested mainly in the class implementation and design, whether or not it has any major drawbacks and inefficiencies. But I will appreciate any given recommendations, warnings and advises.



The creation of the class is only for learning purposes, production usage is not intended.



Date.h:



#pragma once
#ifndef _DATE_H_
#define _DATE_H_

#include <iostream>
#include <string>

class Date
public:
struct BadDate;
enum Month
jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
;

private:
static std::string monthNames [12];
static bool defaultSet;
static Date defaultDate;

int _day, _month, _year;
bool leapYear(const int) const;
void fillDate(int d, Month m, int y);
void checkIllFormed();
bool isIllFormed() const;
void setVal(int&, const int, const int);

public:
static void setDefault(const int d = 1, Month = Month(1), const int y = 2000);
static void showDefault(std::ostream& os);
static const std::string monthNameByNumber(const int);

Date(
int d = (Date::defaultSet)?Date::defaultDate.day():1,
Month m = (Date::defaultSet)?Date::defaultDate.month():Month(1),
int y = (Date::defaultSet)?Date::defaultDate.year():2000
);
Date(int d, int m, int y);
Date(const Date&);
Date& operator=(const Date&);
~Date();

int day() const;
Month month() const;
int year() const;

const std::string getMonthName() const;

void setDay(const int);
void setMonth(const int);
void setYear(const int);

const Date& operator++();
const Date operator++(int);
const Date& operator--();
const Date operator--(int);
;

struct Date::BadDate
int _day, _month, _year;
BadDate(int d, int m, int y);
;

std::ostream& operator<<(std::ostream&, const Date&);
std::ostream& operator<<(std::ostream&, const Date::BadDate&);

#endif


Date.cpp:



#include "Date.h"

Date::BadDate::BadDate(int d, int m, int y)
: _day(d), _month(m), _year(y)

;

std::string Date::monthNames[12] =
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
;
bool Date::defaultSet = true;
Date Date::defaultDate = Date(1, 1, 2000);

void Date::setDefault(const int d, Month m, const int y)
Date::defaultDate.setDay(d);
Date::defaultDate.setMonth(m);
Date::defaultDate.setYear(y);
Date::defaultSet = true;

void Date::showDefault(std::ostream& os)
os << Date::defaultDate;

const std::string Date::monthNameByNumber(const int n)
return Date::monthNames[n-1];


Date::Date(int d, Month m, int y)
: _day(d), _month(m), _year(y)
checkIllFormed();

Date::Date(int d, int m, int y)
: _day(d), _month(Month(m)), _year(y)
checkIllFormed();

Date::Date(const Date& that)
: _day(that.day()), _month(that.month()), _year(that.year())
checkIllFormed();

void Date::checkIllFormed()
if(isIllFormed())
BadDate bd = BadDate(day(), month(), year());
*this = Date::defaultDate;
throw bd;


Date& Date::operator=(const Date& that)
fillDate(that.day(), that.month(), that.year());
return *this;

Date::~Date(void)



int Date::day() const
return _day;

Date::Month Date::month() const
return Month(_month);

int Date::year() const
return _year;


const std::string Date::getMonthName() const
return Date::monthNameByNumber(month());


void Date::setDay(const int d)
setVal(_day, d, day());

void Date::setMonth(const int m)
setVal(_month, m, month());

void Date::setYear(const int y)
setVal(_year, y, year());

void Date::setVal(int& val, const int newVal, const int prevVal)
val = newVal;
if(isIllFormed())
BadDate bd = BadDate(day(), month(), year());
val = prevVal;
throw bd;



const Date& Date::operator++()
setDay(day() + 1);
return *this;

const Date Date::operator++(int)
setDay(day() + 1);
return *this;

const Date& Date::operator--()
setDay(day() - 1);
return *this;

const Date Date::operator--(int)
setDay(day() - 1);
return *this;


std::ostream& operator<<(std::ostream& os, const Date& d)
os << d.day() << '.' << d.getMonthName() << '.' << d.year();
return os;

std::ostream& operator<<(std::ostream& os, const Date::BadDate& bd)
os << bd._day << '.' << Date::monthNameByNumber(bd._month) << '.' << bd._year;
return os;


bool Date::leapYear(const int y) const
/*
1.If the year is evenly divisible by 4, go to step 2. Otherwise, go to step 5.
2.If the year is evenly divisible by 100, go to step 3. Otherwise, go to step 4.
3.If the year is evenly divisible by 400, go to step 4. Otherwise, go to step 5.
4.The year is a leap year (it has 366 days).
5.The year is not a leap year (it has 365 days).
*/
if(y%4)
return false;

if(y%100)
return true;

if(y%400)
return false;

return true;

void Date::fillDate(int d, Month m, int y)
setDay(d);
setMonth(m);
setYear(y);


bool Date::isIllFormed() const


I saw this other question, but my Date class is different and may have its own pitfalls which I am not aware of and I would like to know.







share|improve this question













Here is my implementation of a Date class.



I am interested mainly in the class implementation and design, whether or not it has any major drawbacks and inefficiencies. But I will appreciate any given recommendations, warnings and advises.



The creation of the class is only for learning purposes, production usage is not intended.



Date.h:



#pragma once
#ifndef _DATE_H_
#define _DATE_H_

#include <iostream>
#include <string>

class Date
public:
struct BadDate;
enum Month
jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
;

private:
static std::string monthNames [12];
static bool defaultSet;
static Date defaultDate;

int _day, _month, _year;
bool leapYear(const int) const;
void fillDate(int d, Month m, int y);
void checkIllFormed();
bool isIllFormed() const;
void setVal(int&, const int, const int);

public:
static void setDefault(const int d = 1, Month = Month(1), const int y = 2000);
static void showDefault(std::ostream& os);
static const std::string monthNameByNumber(const int);

Date(
int d = (Date::defaultSet)?Date::defaultDate.day():1,
Month m = (Date::defaultSet)?Date::defaultDate.month():Month(1),
int y = (Date::defaultSet)?Date::defaultDate.year():2000
);
Date(int d, int m, int y);
Date(const Date&);
Date& operator=(const Date&);
~Date();

int day() const;
Month month() const;
int year() const;

const std::string getMonthName() const;

void setDay(const int);
void setMonth(const int);
void setYear(const int);

const Date& operator++();
const Date operator++(int);
const Date& operator--();
const Date operator--(int);
;

struct Date::BadDate
int _day, _month, _year;
BadDate(int d, int m, int y);
;

std::ostream& operator<<(std::ostream&, const Date&);
std::ostream& operator<<(std::ostream&, const Date::BadDate&);

#endif


Date.cpp:



#include "Date.h"

Date::BadDate::BadDate(int d, int m, int y)
: _day(d), _month(m), _year(y)

;

std::string Date::monthNames[12] =
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
;
bool Date::defaultSet = true;
Date Date::defaultDate = Date(1, 1, 2000);

void Date::setDefault(const int d, Month m, const int y)
Date::defaultDate.setDay(d);
Date::defaultDate.setMonth(m);
Date::defaultDate.setYear(y);
Date::defaultSet = true;

void Date::showDefault(std::ostream& os)
os << Date::defaultDate;

const std::string Date::monthNameByNumber(const int n)
return Date::monthNames[n-1];


Date::Date(int d, Month m, int y)
: _day(d), _month(m), _year(y)
checkIllFormed();

Date::Date(int d, int m, int y)
: _day(d), _month(Month(m)), _year(y)
checkIllFormed();

Date::Date(const Date& that)
: _day(that.day()), _month(that.month()), _year(that.year())
checkIllFormed();

void Date::checkIllFormed()
if(isIllFormed())
BadDate bd = BadDate(day(), month(), year());
*this = Date::defaultDate;
throw bd;


Date& Date::operator=(const Date& that)
fillDate(that.day(), that.month(), that.year());
return *this;

Date::~Date(void)



int Date::day() const
return _day;

Date::Month Date::month() const
return Month(_month);

int Date::year() const
return _year;


const std::string Date::getMonthName() const
return Date::monthNameByNumber(month());


void Date::setDay(const int d)
setVal(_day, d, day());

void Date::setMonth(const int m)
setVal(_month, m, month());

void Date::setYear(const int y)
setVal(_year, y, year());

void Date::setVal(int& val, const int newVal, const int prevVal)
val = newVal;
if(isIllFormed())
BadDate bd = BadDate(day(), month(), year());
val = prevVal;
throw bd;



const Date& Date::operator++()
setDay(day() + 1);
return *this;

const Date Date::operator++(int)
setDay(day() + 1);
return *this;

const Date& Date::operator--()
setDay(day() - 1);
return *this;

const Date Date::operator--(int)
setDay(day() - 1);
return *this;


std::ostream& operator<<(std::ostream& os, const Date& d)
os << d.day() << '.' << d.getMonthName() << '.' << d.year();
return os;

std::ostream& operator<<(std::ostream& os, const Date::BadDate& bd)
os << bd._day << '.' << Date::monthNameByNumber(bd._month) << '.' << bd._year;
return os;


bool Date::leapYear(const int y) const
/*
1.If the year is evenly divisible by 4, go to step 2. Otherwise, go to step 5.
2.If the year is evenly divisible by 100, go to step 3. Otherwise, go to step 4.
3.If the year is evenly divisible by 400, go to step 4. Otherwise, go to step 5.
4.The year is a leap year (it has 366 days).
5.The year is not a leap year (it has 365 days).
*/
if(y%4)
return false;

if(y%100)
return true;

if(y%400)
return false;

return true;

void Date::fillDate(int d, Month m, int y)
setDay(d);
setMonth(m);
setYear(y);


bool Date::isIllFormed() const


I saw this other question, but my Date class is different and may have its own pitfalls which I am not aware of and I would like to know.









share|improve this question












share|improve this question




share|improve this question








edited Feb 24 at 15:18









200_success

123k14142399




123k14142399









asked Feb 24 at 11:47









foolcool

636




636











  • Have you implemented this only as an exercise? If not, why not try Boost DateTime?
    – einpoklum
    Feb 25 at 11:30










  • You might also like to take a look at Howard Hinnant's date library which is based on std::chrono.
    – user673679
    Feb 25 at 16:16

















  • Have you implemented this only as an exercise? If not, why not try Boost DateTime?
    – einpoklum
    Feb 25 at 11:30










  • You might also like to take a look at Howard Hinnant's date library which is based on std::chrono.
    – user673679
    Feb 25 at 16:16
















Have you implemented this only as an exercise? If not, why not try Boost DateTime?
– einpoklum
Feb 25 at 11:30




Have you implemented this only as an exercise? If not, why not try Boost DateTime?
– einpoklum
Feb 25 at 11:30












You might also like to take a look at Howard Hinnant's date library which is based on std::chrono.
– user673679
Feb 25 at 16:16





You might also like to take a look at Howard Hinnant's date library which is based on std::chrono.
– user673679
Feb 25 at 16:16











4 Answers
4






active

oldest

votes

















up vote
17
down vote



accepted










1. Header guards



#pragma once
#ifndef _DATE_H_
#define _DATE_H_
#endif


The "classic" header guard and #pragma once are redundant. Use either one of them. If your code should be maximum portable, remove the #pragma once it's not supported by all compilers, while the #ifndef #define #endif sequence is.



Also note that symbols starting with a prefixed _ are reserved for compiler implementation internals in c++.



2. Unnecessary scope qualifying



Statements like



 Date::defaultDate.setDay(d);


inside of any class member functions don't need to qualify the scope of the class member unless you need to qualify an inherited class member or such.



You can simply write



 defaultDate.setDay(d);


there.



3. Correct use of const



The use of const in this function definition



const std::string Date::monthNameByNumber(const int n) 
return Date::monthNames[n-1];



is wrong, or at least doesn't have any benefical effect.



It should rather be



const std::string& Date::monthNameByNumber(int n) const 
// ^^^^^ No change of Date's
// internal state is
// guaranteed.
return Date::monthNames[n-1];



or made a static class member function at all:



static const std::string& monthNameByNumber(unsigned int n) 
static const std::string monthNames =
"January"s, "February"s, "March"s,
"April"s, "May"s, "June"s,
"July"s, "August"s, "September"s,
"October"s, "November"s, "December"s
;
if(n >= (sizeof(monthNames)/sizeof(std::string)))
std::ostringstream msg;
msg << "Valid range of n is [0-" << sizeof(monthNames)/sizeof(std::string) << "]";
throw std::out_of_range(msg.str());

return monthNames[n];



4. Avoid unnecessary casts



You have declared an enum type for the representation of months, but then you prefer to cast instead of just using it:



Month = Month(1),


That should be



Month = jan,


Casting there defeats the whole purpose of using enum values to enhance type safety and readability of the code.



5. Use unsigned for values always greater or equal zero



Here you never expect negative values



int _day, _month, _year;


so these variables should be declared as unsigned:



unsigned int _day, _month, _year;


Also avoid multiple variable definitions in a single line for sake of clarity and readability:



unsigned int day_;
unsigned int month_;
unsigned year_;


Note I moved the _ to the end of the names, same reasoning as stated in my point 1.



6. Getter/Setter naming style



You should improve the naming style of your getter/setter functions. These can be just overloaded function signatures:



 class Date 
unsigned int day_;
// ...
public:
unsigned int day() const; // Getter signature
unsigned int day(unsigned int); // Setter signature (returns the old value)
;


The standard library uses that style a lot, have a look at e.g. the stream flag getters and setters.



7. Use std::exception descendants when throwing



When ever you are going to throw exceptions use either std::exception or a class inheriting it.



So instead of



 BadDate bd = BadDate(day(), month(), year());
val = prevVal;
throw bd;


you should create a class



 class BadDateException : public std::exception 
std::string message;
Date badDate;
public:
BadDateException(const Date& d) : badDate(d)
std::ostringstream msg;
msg << "Date " << d << " isn't a valid date.";
message = msg.str();

const char* what()
return message.c_str();




thus the clients can handle exception in a more generic way and don't need to know about your BadDate class:



 try 
Date d;
std::cin >> d;

catch(const std::exception& e)
std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
exit(1);



If one needs to handle your exception specifically it can be done like



 try 
Date d;
std::cin >> d;

catch(const BadDateException& e)
std::cerr << "The date value is invalid '" << e.what() << "'" << std::endl;
exit(42); // Do something specific for that exception

catch(const std::exception& e)
std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
exit(1);



8. Follow the principle of least astonishment



Your way to pre-configure the default date violates the POLA policy.



I'd leave that feature out completely. It's way more readable and clear to use something like



const Date defaultDate(1,Jan,200);

Date d;
if(!std::cin >> d)
d = defaultDate;



or



const Date defaultDate(1,Jan,200);
Date d(defaultDate);


in your local context.



Someone could have used setDefaultDate() elsewhere in a different context, and the client is wondering why simply declaring Date d; uses those specific values.






share|improve this answer



















  • 1




    @πάνταῥεῖ Just a few things: Point 3: In your proposed static version, I think there's good point about using std::array instead of raw C arrays, especially since you can then compare to arr.size() directly instead of relying on the magic number 12. Point 5: You don't really need to move the underscores to the back here. As far as the standard is concerned, everything beginning with an underscore is reserved only in the global namespace. Only names beginning with a underscore followed by a capital letter or beginning with two underscores are reserved in every context.
    – Ben Steffan
    Feb 24 at 16:41






  • 1




    @πάνταῥεῖ: I disagree with the naming style. I find a clear setXxx (or set_xxx) a much stronger signal that the operation is mutable, whereas day(5) could simply be a getter with a parameter.
    – Matthieu M.
    Feb 24 at 18:25






  • 1




    @πάνταῥεῖ: I also disagree with the unsigned advice; nobody wants modulo arithmetic on date components, it's only surprising :(
    – Matthieu M.
    Feb 24 at 18:25






  • 1




    "symbols starting with a prefixed _ are reserved..." Actually not quite right. Symbols starting with _ then a capital letter are reserved (_Capital, _DATE_H_, etc). But if they are _lowercase, they are only reserved in the global namespace; they are not reserved at class scope. I wouldn't do it in a class scope anyway, so I don't have to remember the exact rules, but _member is technically fine.
    – Justin
    Feb 24 at 19:40







  • 2




    @einpoklum I'm still convinced that how all the standard c++ library functions do it is fine.
    – Ï€Î¬Î½Ï„α ῥεῖ
    Feb 25 at 11:55

















up vote
8
down vote













1. Header Names



I recommend using .hpp for purely C++ header files, and reserving .h to C-compatible header files.



2. Header Guards



As mentioned, you should #pragma once rather than #ifndef.



If you really wish to use the error prone header guards; then I recommend following the Boost convention for naming: PROJECT_PATH_FILE_INCLUDED which helps prevent clashes in header guard names. It's significantly more work, of course...



3. Namespace



By defining symbols in the global namespace, you open your code up to conflicts with C libraries, or other uncouth C++ libraries.



Instead, decide on a namespace, generally the project name, and wrap any single item you define in this namespace.



4. Use enum class, and pick an underlying type.



Any new enum should really be enum class, which introduces a scope for the enumerators rather than injecting them in the surrounding scope.



Since you are no longer polluting the surrounding namespace, you can without fear pull out Month at namespace level, making it easier for people to name the type.



I also encourage you to pick a type. By default the compiler will use int, which is... 3 bytes too wide in your case. You can easily use int8_t instead, it is large enough.



Also, you might as well dispense with pointless abbreviations.



This gives:



#include <cstdint>

namespace mydate
enum class Month: std::int8_t
January = 1, February, March, April, May, June,
July, August, September, October, November, December,
;



As a side bonus, enum class can be forward declared by your users.



5. Exception should inherit std::exception.



It's a convention that most everyone follows which allows catching all exceptions and being able to display something with .what() which beats ... hands down.



6. Public first, please.



Header files are often peeked at by your clients.



Therefore, it is a good idea to expose your class interface in the order: public, protected, private. This minimizes the number of irrelevant lines that the user has to skip.



7. There shall be no global state.



Global state is a plague on software as it prevents local reasoning, the primary tool you have to understand how a piece of software works.



It also suffers from data-races, however just slapping thread_local is not a panacea either: it doesn't solve the local reasoning issue and prevents clients from using different threading models such as coroutines.



In your case, it's even worse, because there is no good default date which will satisfy every user.



8. Global constants are good.



But they shall be constants, hence:



static std::array<std::string, 12> const monthNames;


Using std::array is preferred. You get some goodies, such as bounds checking, easy extraction of size, etc...



9. Pick your types.



int is a default type, but it is not very sensible.



You already have a Month enum, why is not used for _month?



I also recommend using a Day type, or at least using std::int8_t again since there's no reason to have a day greater than 127.



10. Stateless helper functions.



The leapYear function does not access this, so it need not be a class method.



You can either propose it as a free-function as part of your interface, or otherwise only declare it in the .cpp file in an anonymous namespace.



11. Never return a const value.



It's rather pointless, as the client can copy it anyway.



You may wish to return a T const& (const-reference). Unfortunately it means that clients get to know that your class contains a T, so that if you ever want to change the internals of your class, clients will break.



12. Defaults are hard.



If there is no meaningful default value for your type, consider NOT defining a default constructor.



This is pretty restrictive, of course, so you may consider instead:



  • defining an invalid value, though this means that (1) your class must now have a isValid method and (2) on each getter your class must check whether it is valid.

  • defining a default value nonetheless, and document it. In this case, it may be a good to pick an unlikely value, such as the 1st January of the year 1, to help clients realize when they forgot to set the value.

13. Beware typeless interfaces.



The US format for dates is MM/DD/YYYY, the ISO format is YYYY/MM/DD. When you present a constructor Date(int, int, int), it's very easy for a client to accidentally pass the arguments in the wrong order.



In this sense, Date(int, Month, int) is a step up, since Americans cannot accidentally use their weird format, however it's still possible to accidentally use ISO instead of DD/MM/YYYY as you intend.



There are 2 possibilities out of there:



  • Only offer strongly typed arguments: Date(Day, Month, Year),

  • Use named constructors instead.

The latter is done with static methods:



static Date from_date_month_year(int day, Month month, int year);


Note: while it's relatively clear that int day will have a range [1, 31], it's unclear whether int month has a range [0, 11] or [1, 12]; don't tempt the user.



14. Name your parameters.



Here, d is probably understood, but in general please attempt to give your parameters full names. Abbreviations are really not necessary in general.



15. Rule of Zero



The Rule of Zero states that you should rarely (if ever) define a Copy Constructor, Copy Assignment Operator, Move Constructor, Move Assignment Operator or Destructor.



It is only necessary on technical resource management classes, such as unique_ptr or vector. If you ever find yourself writing such a class, be sure to make it as minimal as possible; without any business logic.



16. Be consistent.



Your interface sometimes use int for month (where it's ambiguous) and sometimes Month. Stick to Month.



17. Follow conventions.



The return type of operator++ and operator++(int) is never const-qualified, to allow chaining.



18. Be sparse in your includes.



You never use ostream, so you may as well only include <iosfwd> in your header.



Minimizing the amount of includes in your headers help with compilation times.




I'll stop here, as that's a lot to fix already.



Congratulations on writing your Date class :)






share|improve this answer























  • @Justin: Good point; let me put a meaningful name.
    – Matthieu M.
    Feb 24 at 20:28










  • It's a good name now. I'd probably still write from_dmy, since I tend to dislike names that are that long and dmy is clear enough when talking about dates, but naming is really subjective so it's really just preference at that point
    – Justin
    Feb 24 at 20:37










  • #13 I would recommend year, month, day order as it is the usual convention. Month should have a range of 1 to 12. When someone write a date like 2018-02-25, it is clear that 2 is for February and not March so code should follow that convention.
    – Phil1970
    Feb 25 at 16:15










  • @Phil1970: I would personally recommend multiple named constructors, with varied formats. This is the point of named constructors, after all. I simply limited myself to show to the OP how to rewrite their existing constructor as a static factory method.
    – Matthieu M.
    Feb 25 at 16:19










  • Strongly disagree on point 2, until you can add a reference to the relevant section of a C++ standard that specifies #pragma once.
    – Toby Speight
    Feb 27 at 9:31

















up vote
6
down vote













[code] header guard



Personally I'd recommend just using #pragma once. It's supported by most compilers, it's less typing, less work for the preprocessor, and much less error prone (renaming files and forgetting to rename the define, issues with reserved names, typos in the definition / check, etc.)



[design] default



The static default functionality probably shouldn't be inside the class as it's error prone and assumes the users needs (e.g. What if they need two different defaults? They'd have to keep changing it backwards and forwards, which makes it pointless).



If the user wants a default value, they can store a constant Date instance somewhere themselves, and pass that it into the constructor easily enough. This also makes it instantly obvious what value an intialized object is:



const Date defaultDate(23, Date::Month::mar, 3);

Date d1; // no idea what value this has... it depends what the default is at the moment! we now have to find out if this is set anywhere else in the codebase, AND then understand which code paths lead from there to here...
Date d2(defaultDate); // it's set to the value of defaultDate!


[design] checking values



Presumably, the idea is to prevent creation of an invalid date, and have a transactional interface (i.e. changing the date either works, or throws an exception, leaving the current value untouched.



(Bug:) However, the way fillDate is currently defined, we try to set each component individually, then check if the whole date object is still valid at each step. This may cause problems, because an intermediate date may be valid (and so we successfully change the day or month), but not the final date. This means that although we throw an error, the initial date value has still been changed.



Furthermore, setting a value, then checking, then setting it back again and throwing is inefficient.



Instead we could just pass the numbers to be checked to a checking function (i.e. isIllFormed) before setting anything. This can also simplify construction and assignment:



Date::Date(): // default constructor provided
_day(1), _month(1), _year(2000)

Date::Date(int d, int m, int y)
: Date() // use delegating constructor then call a single point of checking / setting

set(d, m, y);


Date::Date(const Date& that)
: Date()

set(that.day(), that.month(), that.year());


Date& Date::operator=(const Date& that)

set(that.day(), that.month(), that.year());

return *this;


void Date::set(int day, int month, int year)

if (!isValidDate(day, month, year)) // this check and the throw could be perhaps abstracted into a throwIfInvalid function...
throw BadDate(day, month, year);

_day = day;
_month = month;
_year = year;


void Date::setDay(const int d)

if (!isValidDate(d, month(), year()))
throw BadDate(d, month(), year());

_day = d;

void Date::setMonth(const int m)

if (!isValidDate(day(), m, year()))
throw BadDate(day(), m, year());

_month = m;

void Date::setYear(const int y)

if (!isValidDate(day(), month(), y))
throw BadDate(day(), month(), y);

_year = y;


// isValidDate is just isIllFormed with the logic reversed


isValidDate and isLeapYear can be static functions, and might as well be public, since they'd be useful for users who need to validate input before creating a date class:



static bool isValidDate(int d, int m, int y);
static bool isLeapYear(int y);





share|improve this answer




























    up vote
    2
    down vote













    In addition to the many points raised in the 3 existing answers, there are still a lot of other issues in that design.



    Assignment



    1. Operator = is inefficient. If validation was properly done with exception so that it is impossible to create an invalid date, then you won't need validation… And even if you want validation, you don't need to do it 3 times.


    2. Individual functions like setDay, setMonth and setYear does not make much sense for a date. You almost always want to set all 3 components at once. And for the very few times you would want to update only some components, you can always read parts that do no not change.


    Construction



    1. Constructor order is unusual. Almost everyone would assume that the order is year, month and day and doing otherwise would only cause many bugs.


    2. Your customizable default value does not make much sense. Also, it is not thread safe. If you need some predefined values, you make some constants. Default date would usually be the minimum supported date (for ex. 0001-01-01).


    3. Also, you should not have default values for each parameter on the constructor taking an int, a Month and another int. The default would very rarely be appropriate so if someone create a date with some defaults, then the code would be harder to understand. Also as your constructor is not explicit, it can lead to undesirable conversion constructor being called in some contexts.


    Increment and decrement operators



    1. Your postfix operators do not returns the old value as expected. As already mentioned by others, return type should not be const. You should almost always mimic what is done on predefined types.


    2. Your operators does not properly handle month and year changes. If you provide such operators, then at least you should make sure that incrementing last day of a month will return first day of next month. And for decrementing, you should go to last day of previous month. And you probably also want to ensure that your date stays within the supported range.



    3. It is recommended to code the postfix operator using the prefix operator to avoid code duplication. You can easily write something like:



      Date operator++(int)

      Date result(*this); // Make a copy
      ++*this; // Increment this object
      return result; // Return old value



    Readability



    1. Use spacing in expression. Your should have a space around every binary operator in an expression (and also after branching keywords). So for ex. if(y%4) should really be if (y % 4).


    2. You should avoid short variable names like d. By writing day instead, the code is much easier to read.


    3. You should not write void for empty parameter lists in C++ like you did in: Date::~Date(void). Even worse, your definition is inconsistent with your declaration.


    Validation issues



    1. If you try to modify multiple parts of a date, like going from 2017-03-31 to 2016-02-29, it would fail if you do validation after setting the day and month but not the year.


    2. You should also use assertion in your code to verify that you get expected results or that the code is properly used.


    Unit testing



    1. Before writing a library like this, you should write unit tests. Many problems like incorrect postfix operator result can easily be detected with a properly written test.

    Conclusion



    Given the high number of issues (between 30 and 45 issues), I would recommend you to read a bunch of good C++ books before writing more "library" code. I highly recommend Meyers and Sutter books among others.



    Also other libraries could be used as a source of inspiration even in other language like DateTime struct in .NET. In that case, value is immutable which is an even more valuable design.






    share|improve this answer























      Your Answer




      StackExchange.ifUsing("editor", function ()
      return StackExchange.using("mathjaxEditing", function ()
      StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
      StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
      );
      );
      , "mathjax-editing");

      StackExchange.ifUsing("editor", function ()
      StackExchange.using("externalEditor", function ()
      StackExchange.using("snippets", function ()
      StackExchange.snippets.init();
      );
      );
      , "code-snippets");

      StackExchange.ready(function()
      var channelOptions =
      tags: "".split(" "),
      id: "196"
      ;
      initTagRenderer("".split(" "), "".split(" "), channelOptions);

      StackExchange.using("externalEditor", function()
      // Have to fire editor after snippets, if snippets enabled
      if (StackExchange.settings.snippets.snippetsEnabled)
      StackExchange.using("snippets", function()
      createEditor();
      );

      else
      createEditor();

      );

      function createEditor()
      StackExchange.prepareEditor(
      heartbeatType: 'answer',
      convertImagesToLinks: false,
      noModals: false,
      showLowRepImageUploadWarning: true,
      reputationToPostImages: null,
      bindNavPrevention: true,
      postfix: "",
      onDemand: true,
      discardSelector: ".discard-answer"
      ,immediatelyShowMarkdownHelp:true
      );



      );








       

      draft saved


      draft discarded


















      StackExchange.ready(
      function ()
      StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f188263%2fdate-class-implementation-in-c%23new-answer', 'question_page');

      );

      Post as a guest






























      4 Answers
      4






      active

      oldest

      votes








      4 Answers
      4






      active

      oldest

      votes









      active

      oldest

      votes






      active

      oldest

      votes








      up vote
      17
      down vote



      accepted










      1. Header guards



      #pragma once
      #ifndef _DATE_H_
      #define _DATE_H_
      #endif


      The "classic" header guard and #pragma once are redundant. Use either one of them. If your code should be maximum portable, remove the #pragma once it's not supported by all compilers, while the #ifndef #define #endif sequence is.



      Also note that symbols starting with a prefixed _ are reserved for compiler implementation internals in c++.



      2. Unnecessary scope qualifying



      Statements like



       Date::defaultDate.setDay(d);


      inside of any class member functions don't need to qualify the scope of the class member unless you need to qualify an inherited class member or such.



      You can simply write



       defaultDate.setDay(d);


      there.



      3. Correct use of const



      The use of const in this function definition



      const std::string Date::monthNameByNumber(const int n) 
      return Date::monthNames[n-1];



      is wrong, or at least doesn't have any benefical effect.



      It should rather be



      const std::string& Date::monthNameByNumber(int n) const 
      // ^^^^^ No change of Date's
      // internal state is
      // guaranteed.
      return Date::monthNames[n-1];



      or made a static class member function at all:



      static const std::string& monthNameByNumber(unsigned int n) 
      static const std::string monthNames =
      "January"s, "February"s, "March"s,
      "April"s, "May"s, "June"s,
      "July"s, "August"s, "September"s,
      "October"s, "November"s, "December"s
      ;
      if(n >= (sizeof(monthNames)/sizeof(std::string)))
      std::ostringstream msg;
      msg << "Valid range of n is [0-" << sizeof(monthNames)/sizeof(std::string) << "]";
      throw std::out_of_range(msg.str());

      return monthNames[n];



      4. Avoid unnecessary casts



      You have declared an enum type for the representation of months, but then you prefer to cast instead of just using it:



      Month = Month(1),


      That should be



      Month = jan,


      Casting there defeats the whole purpose of using enum values to enhance type safety and readability of the code.



      5. Use unsigned for values always greater or equal zero



      Here you never expect negative values



      int _day, _month, _year;


      so these variables should be declared as unsigned:



      unsigned int _day, _month, _year;


      Also avoid multiple variable definitions in a single line for sake of clarity and readability:



      unsigned int day_;
      unsigned int month_;
      unsigned year_;


      Note I moved the _ to the end of the names, same reasoning as stated in my point 1.



      6. Getter/Setter naming style



      You should improve the naming style of your getter/setter functions. These can be just overloaded function signatures:



       class Date 
      unsigned int day_;
      // ...
      public:
      unsigned int day() const; // Getter signature
      unsigned int day(unsigned int); // Setter signature (returns the old value)
      ;


      The standard library uses that style a lot, have a look at e.g. the stream flag getters and setters.



      7. Use std::exception descendants when throwing



      When ever you are going to throw exceptions use either std::exception or a class inheriting it.



      So instead of



       BadDate bd = BadDate(day(), month(), year());
      val = prevVal;
      throw bd;


      you should create a class



       class BadDateException : public std::exception 
      std::string message;
      Date badDate;
      public:
      BadDateException(const Date& d) : badDate(d)
      std::ostringstream msg;
      msg << "Date " << d << " isn't a valid date.";
      message = msg.str();

      const char* what()
      return message.c_str();




      thus the clients can handle exception in a more generic way and don't need to know about your BadDate class:



       try 
      Date d;
      std::cin >> d;

      catch(const std::exception& e)
      std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
      exit(1);



      If one needs to handle your exception specifically it can be done like



       try 
      Date d;
      std::cin >> d;

      catch(const BadDateException& e)
      std::cerr << "The date value is invalid '" << e.what() << "'" << std::endl;
      exit(42); // Do something specific for that exception

      catch(const std::exception& e)
      std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
      exit(1);



      8. Follow the principle of least astonishment



      Your way to pre-configure the default date violates the POLA policy.



      I'd leave that feature out completely. It's way more readable and clear to use something like



      const Date defaultDate(1,Jan,200);

      Date d;
      if(!std::cin >> d)
      d = defaultDate;



      or



      const Date defaultDate(1,Jan,200);
      Date d(defaultDate);


      in your local context.



      Someone could have used setDefaultDate() elsewhere in a different context, and the client is wondering why simply declaring Date d; uses those specific values.






      share|improve this answer



















      • 1




        @πάνταῥεῖ Just a few things: Point 3: In your proposed static version, I think there's good point about using std::array instead of raw C arrays, especially since you can then compare to arr.size() directly instead of relying on the magic number 12. Point 5: You don't really need to move the underscores to the back here. As far as the standard is concerned, everything beginning with an underscore is reserved only in the global namespace. Only names beginning with a underscore followed by a capital letter or beginning with two underscores are reserved in every context.
        – Ben Steffan
        Feb 24 at 16:41






      • 1




        @πάνταῥεῖ: I disagree with the naming style. I find a clear setXxx (or set_xxx) a much stronger signal that the operation is mutable, whereas day(5) could simply be a getter with a parameter.
        – Matthieu M.
        Feb 24 at 18:25






      • 1




        @πάνταῥεῖ: I also disagree with the unsigned advice; nobody wants modulo arithmetic on date components, it's only surprising :(
        – Matthieu M.
        Feb 24 at 18:25






      • 1




        "symbols starting with a prefixed _ are reserved..." Actually not quite right. Symbols starting with _ then a capital letter are reserved (_Capital, _DATE_H_, etc). But if they are _lowercase, they are only reserved in the global namespace; they are not reserved at class scope. I wouldn't do it in a class scope anyway, so I don't have to remember the exact rules, but _member is technically fine.
        – Justin
        Feb 24 at 19:40







      • 2




        @einpoklum I'm still convinced that how all the standard c++ library functions do it is fine.
        – Ï€Î¬Î½Ï„α ῥεῖ
        Feb 25 at 11:55














      up vote
      17
      down vote



      accepted










      1. Header guards



      #pragma once
      #ifndef _DATE_H_
      #define _DATE_H_
      #endif


      The "classic" header guard and #pragma once are redundant. Use either one of them. If your code should be maximum portable, remove the #pragma once it's not supported by all compilers, while the #ifndef #define #endif sequence is.



      Also note that symbols starting with a prefixed _ are reserved for compiler implementation internals in c++.



      2. Unnecessary scope qualifying



      Statements like



       Date::defaultDate.setDay(d);


      inside of any class member functions don't need to qualify the scope of the class member unless you need to qualify an inherited class member or such.



      You can simply write



       defaultDate.setDay(d);


      there.



      3. Correct use of const



      The use of const in this function definition



      const std::string Date::monthNameByNumber(const int n) 
      return Date::monthNames[n-1];



      is wrong, or at least doesn't have any benefical effect.



      It should rather be



      const std::string& Date::monthNameByNumber(int n) const 
      // ^^^^^ No change of Date's
      // internal state is
      // guaranteed.
      return Date::monthNames[n-1];



      or made a static class member function at all:



      static const std::string& monthNameByNumber(unsigned int n) 
      static const std::string monthNames =
      "January"s, "February"s, "March"s,
      "April"s, "May"s, "June"s,
      "July"s, "August"s, "September"s,
      "October"s, "November"s, "December"s
      ;
      if(n >= (sizeof(monthNames)/sizeof(std::string)))
      std::ostringstream msg;
      msg << "Valid range of n is [0-" << sizeof(monthNames)/sizeof(std::string) << "]";
      throw std::out_of_range(msg.str());

      return monthNames[n];



      4. Avoid unnecessary casts



      You have declared an enum type for the representation of months, but then you prefer to cast instead of just using it:



      Month = Month(1),


      That should be



      Month = jan,


      Casting there defeats the whole purpose of using enum values to enhance type safety and readability of the code.



      5. Use unsigned for values always greater or equal zero



      Here you never expect negative values



      int _day, _month, _year;


      so these variables should be declared as unsigned:



      unsigned int _day, _month, _year;


      Also avoid multiple variable definitions in a single line for sake of clarity and readability:



      unsigned int day_;
      unsigned int month_;
      unsigned year_;


      Note I moved the _ to the end of the names, same reasoning as stated in my point 1.



      6. Getter/Setter naming style



      You should improve the naming style of your getter/setter functions. These can be just overloaded function signatures:



       class Date 
      unsigned int day_;
      // ...
      public:
      unsigned int day() const; // Getter signature
      unsigned int day(unsigned int); // Setter signature (returns the old value)
      ;


      The standard library uses that style a lot, have a look at e.g. the stream flag getters and setters.



      7. Use std::exception descendants when throwing



      When ever you are going to throw exceptions use either std::exception or a class inheriting it.



      So instead of



       BadDate bd = BadDate(day(), month(), year());
      val = prevVal;
      throw bd;


      you should create a class



       class BadDateException : public std::exception 
      std::string message;
      Date badDate;
      public:
      BadDateException(const Date& d) : badDate(d)
      std::ostringstream msg;
      msg << "Date " << d << " isn't a valid date.";
      message = msg.str();

      const char* what()
      return message.c_str();




      thus the clients can handle exception in a more generic way and don't need to know about your BadDate class:



       try 
      Date d;
      std::cin >> d;

      catch(const std::exception& e)
      std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
      exit(1);



      If one needs to handle your exception specifically it can be done like



       try 
      Date d;
      std::cin >> d;

      catch(const BadDateException& e)
      std::cerr << "The date value is invalid '" << e.what() << "'" << std::endl;
      exit(42); // Do something specific for that exception

      catch(const std::exception& e)
      std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
      exit(1);



      8. Follow the principle of least astonishment



      Your way to pre-configure the default date violates the POLA policy.



      I'd leave that feature out completely. It's way more readable and clear to use something like



      const Date defaultDate(1,Jan,200);

      Date d;
      if(!std::cin >> d)
      d = defaultDate;



      or



      const Date defaultDate(1,Jan,200);
      Date d(defaultDate);


      in your local context.



      Someone could have used setDefaultDate() elsewhere in a different context, and the client is wondering why simply declaring Date d; uses those specific values.






      share|improve this answer



















      • 1




        @πάνταῥεῖ Just a few things: Point 3: In your proposed static version, I think there's good point about using std::array instead of raw C arrays, especially since you can then compare to arr.size() directly instead of relying on the magic number 12. Point 5: You don't really need to move the underscores to the back here. As far as the standard is concerned, everything beginning with an underscore is reserved only in the global namespace. Only names beginning with a underscore followed by a capital letter or beginning with two underscores are reserved in every context.
        – Ben Steffan
        Feb 24 at 16:41






      • 1




        @πάνταῥεῖ: I disagree with the naming style. I find a clear setXxx (or set_xxx) a much stronger signal that the operation is mutable, whereas day(5) could simply be a getter with a parameter.
        – Matthieu M.
        Feb 24 at 18:25






      • 1




        @πάνταῥεῖ: I also disagree with the unsigned advice; nobody wants modulo arithmetic on date components, it's only surprising :(
        – Matthieu M.
        Feb 24 at 18:25






      • 1




        "symbols starting with a prefixed _ are reserved..." Actually not quite right. Symbols starting with _ then a capital letter are reserved (_Capital, _DATE_H_, etc). But if they are _lowercase, they are only reserved in the global namespace; they are not reserved at class scope. I wouldn't do it in a class scope anyway, so I don't have to remember the exact rules, but _member is technically fine.
        – Justin
        Feb 24 at 19:40







      • 2




        @einpoklum I'm still convinced that how all the standard c++ library functions do it is fine.
        – Ï€Î¬Î½Ï„α ῥεῖ
        Feb 25 at 11:55












      up vote
      17
      down vote



      accepted







      up vote
      17
      down vote



      accepted






      1. Header guards



      #pragma once
      #ifndef _DATE_H_
      #define _DATE_H_
      #endif


      The "classic" header guard and #pragma once are redundant. Use either one of them. If your code should be maximum portable, remove the #pragma once it's not supported by all compilers, while the #ifndef #define #endif sequence is.



      Also note that symbols starting with a prefixed _ are reserved for compiler implementation internals in c++.



      2. Unnecessary scope qualifying



      Statements like



       Date::defaultDate.setDay(d);


      inside of any class member functions don't need to qualify the scope of the class member unless you need to qualify an inherited class member or such.



      You can simply write



       defaultDate.setDay(d);


      there.



      3. Correct use of const



      The use of const in this function definition



      const std::string Date::monthNameByNumber(const int n) 
      return Date::monthNames[n-1];



      is wrong, or at least doesn't have any benefical effect.



      It should rather be



      const std::string& Date::monthNameByNumber(int n) const 
      // ^^^^^ No change of Date's
      // internal state is
      // guaranteed.
      return Date::monthNames[n-1];



      or made a static class member function at all:



      static const std::string& monthNameByNumber(unsigned int n) 
      static const std::string monthNames =
      "January"s, "February"s, "March"s,
      "April"s, "May"s, "June"s,
      "July"s, "August"s, "September"s,
      "October"s, "November"s, "December"s
      ;
      if(n >= (sizeof(monthNames)/sizeof(std::string)))
      std::ostringstream msg;
      msg << "Valid range of n is [0-" << sizeof(monthNames)/sizeof(std::string) << "]";
      throw std::out_of_range(msg.str());

      return monthNames[n];



      4. Avoid unnecessary casts



      You have declared an enum type for the representation of months, but then you prefer to cast instead of just using it:



      Month = Month(1),


      That should be



      Month = jan,


      Casting there defeats the whole purpose of using enum values to enhance type safety and readability of the code.



      5. Use unsigned for values always greater or equal zero



      Here you never expect negative values



      int _day, _month, _year;


      so these variables should be declared as unsigned:



      unsigned int _day, _month, _year;


      Also avoid multiple variable definitions in a single line for sake of clarity and readability:



      unsigned int day_;
      unsigned int month_;
      unsigned year_;


      Note I moved the _ to the end of the names, same reasoning as stated in my point 1.



      6. Getter/Setter naming style



      You should improve the naming style of your getter/setter functions. These can be just overloaded function signatures:



       class Date 
      unsigned int day_;
      // ...
      public:
      unsigned int day() const; // Getter signature
      unsigned int day(unsigned int); // Setter signature (returns the old value)
      ;


      The standard library uses that style a lot, have a look at e.g. the stream flag getters and setters.



      7. Use std::exception descendants when throwing



      When ever you are going to throw exceptions use either std::exception or a class inheriting it.



      So instead of



       BadDate bd = BadDate(day(), month(), year());
      val = prevVal;
      throw bd;


      you should create a class



       class BadDateException : public std::exception 
      std::string message;
      Date badDate;
      public:
      BadDateException(const Date& d) : badDate(d)
      std::ostringstream msg;
      msg << "Date " << d << " isn't a valid date.";
      message = msg.str();

      const char* what()
      return message.c_str();




      thus the clients can handle exception in a more generic way and don't need to know about your BadDate class:



       try 
      Date d;
      std::cin >> d;

      catch(const std::exception& e)
      std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
      exit(1);



      If one needs to handle your exception specifically it can be done like



       try 
      Date d;
      std::cin >> d;

      catch(const BadDateException& e)
      std::cerr << "The date value is invalid '" << e.what() << "'" << std::endl;
      exit(42); // Do something specific for that exception

      catch(const std::exception& e)
      std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
      exit(1);



      8. Follow the principle of least astonishment



      Your way to pre-configure the default date violates the POLA policy.



      I'd leave that feature out completely. It's way more readable and clear to use something like



      const Date defaultDate(1,Jan,200);

      Date d;
      if(!std::cin >> d)
      d = defaultDate;



      or



      const Date defaultDate(1,Jan,200);
      Date d(defaultDate);


      in your local context.



      Someone could have used setDefaultDate() elsewhere in a different context, and the client is wondering why simply declaring Date d; uses those specific values.






      share|improve this answer















      1. Header guards



      #pragma once
      #ifndef _DATE_H_
      #define _DATE_H_
      #endif


      The "classic" header guard and #pragma once are redundant. Use either one of them. If your code should be maximum portable, remove the #pragma once it's not supported by all compilers, while the #ifndef #define #endif sequence is.



      Also note that symbols starting with a prefixed _ are reserved for compiler implementation internals in c++.



      2. Unnecessary scope qualifying



      Statements like



       Date::defaultDate.setDay(d);


      inside of any class member functions don't need to qualify the scope of the class member unless you need to qualify an inherited class member or such.



      You can simply write



       defaultDate.setDay(d);


      there.



      3. Correct use of const



      The use of const in this function definition



      const std::string Date::monthNameByNumber(const int n) 
      return Date::monthNames[n-1];



      is wrong, or at least doesn't have any benefical effect.



      It should rather be



      const std::string& Date::monthNameByNumber(int n) const 
      // ^^^^^ No change of Date's
      // internal state is
      // guaranteed.
      return Date::monthNames[n-1];



      or made a static class member function at all:



      static const std::string& monthNameByNumber(unsigned int n) 
      static const std::string monthNames =
      "January"s, "February"s, "March"s,
      "April"s, "May"s, "June"s,
      "July"s, "August"s, "September"s,
      "October"s, "November"s, "December"s
      ;
      if(n >= (sizeof(monthNames)/sizeof(std::string)))
      std::ostringstream msg;
      msg << "Valid range of n is [0-" << sizeof(monthNames)/sizeof(std::string) << "]";
      throw std::out_of_range(msg.str());

      return monthNames[n];



      4. Avoid unnecessary casts



      You have declared an enum type for the representation of months, but then you prefer to cast instead of just using it:



      Month = Month(1),


      That should be



      Month = jan,


      Casting there defeats the whole purpose of using enum values to enhance type safety and readability of the code.



      5. Use unsigned for values always greater or equal zero



      Here you never expect negative values



      int _day, _month, _year;


      so these variables should be declared as unsigned:



      unsigned int _day, _month, _year;


      Also avoid multiple variable definitions in a single line for sake of clarity and readability:



      unsigned int day_;
      unsigned int month_;
      unsigned year_;


      Note I moved the _ to the end of the names, same reasoning as stated in my point 1.



      6. Getter/Setter naming style



      You should improve the naming style of your getter/setter functions. These can be just overloaded function signatures:



       class Date 
      unsigned int day_;
      // ...
      public:
      unsigned int day() const; // Getter signature
      unsigned int day(unsigned int); // Setter signature (returns the old value)
      ;


      The standard library uses that style a lot, have a look at e.g. the stream flag getters and setters.



      7. Use std::exception descendants when throwing



      When ever you are going to throw exceptions use either std::exception or a class inheriting it.



      So instead of



       BadDate bd = BadDate(day(), month(), year());
      val = prevVal;
      throw bd;


      you should create a class



       class BadDateException : public std::exception 
      std::string message;
      Date badDate;
      public:
      BadDateException(const Date& d) : badDate(d)
      std::ostringstream msg;
      msg << "Date " << d << " isn't a valid date.";
      message = msg.str();

      const char* what()
      return message.c_str();




      thus the clients can handle exception in a more generic way and don't need to know about your BadDate class:



       try 
      Date d;
      std::cin >> d;

      catch(const std::exception& e)
      std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
      exit(1);



      If one needs to handle your exception specifically it can be done like



       try 
      Date d;
      std::cin >> d;

      catch(const BadDateException& e)
      std::cerr << "The date value is invalid '" << e.what() << "'" << std::endl;
      exit(42); // Do something specific for that exception

      catch(const std::exception& e)
      std::cerr << "Caught exception '" << e.what() << "'" << std::endl;
      exit(1);



      8. Follow the principle of least astonishment



      Your way to pre-configure the default date violates the POLA policy.



      I'd leave that feature out completely. It's way more readable and clear to use something like



      const Date defaultDate(1,Jan,200);

      Date d;
      if(!std::cin >> d)
      d = defaultDate;



      or



      const Date defaultDate(1,Jan,200);
      Date d(defaultDate);


      in your local context.



      Someone could have used setDefaultDate() elsewhere in a different context, and the client is wondering why simply declaring Date d; uses those specific values.







      share|improve this answer















      share|improve this answer



      share|improve this answer








      edited Feb 24 at 16:48


























      answered Feb 24 at 11:55









      πάντα ῥεῖ

      3,82431126




      3,82431126







      • 1




        @πάνταῥεῖ Just a few things: Point 3: In your proposed static version, I think there's good point about using std::array instead of raw C arrays, especially since you can then compare to arr.size() directly instead of relying on the magic number 12. Point 5: You don't really need to move the underscores to the back here. As far as the standard is concerned, everything beginning with an underscore is reserved only in the global namespace. Only names beginning with a underscore followed by a capital letter or beginning with two underscores are reserved in every context.
        – Ben Steffan
        Feb 24 at 16:41






      • 1




        @πάνταῥεῖ: I disagree with the naming style. I find a clear setXxx (or set_xxx) a much stronger signal that the operation is mutable, whereas day(5) could simply be a getter with a parameter.
        – Matthieu M.
        Feb 24 at 18:25






      • 1




        @πάνταῥεῖ: I also disagree with the unsigned advice; nobody wants modulo arithmetic on date components, it's only surprising :(
        – Matthieu M.
        Feb 24 at 18:25






      • 1




        "symbols starting with a prefixed _ are reserved..." Actually not quite right. Symbols starting with _ then a capital letter are reserved (_Capital, _DATE_H_, etc). But if they are _lowercase, they are only reserved in the global namespace; they are not reserved at class scope. I wouldn't do it in a class scope anyway, so I don't have to remember the exact rules, but _member is technically fine.
        – Justin
        Feb 24 at 19:40







      • 2




        @einpoklum I'm still convinced that how all the standard c++ library functions do it is fine.
        – Ï€Î¬Î½Ï„α ῥεῖ
        Feb 25 at 11:55












      • 1




        @πάνταῥεῖ Just a few things: Point 3: In your proposed static version, I think there's good point about using std::array instead of raw C arrays, especially since you can then compare to arr.size() directly instead of relying on the magic number 12. Point 5: You don't really need to move the underscores to the back here. As far as the standard is concerned, everything beginning with an underscore is reserved only in the global namespace. Only names beginning with a underscore followed by a capital letter or beginning with two underscores are reserved in every context.
        – Ben Steffan
        Feb 24 at 16:41






      • 1




        @πάνταῥεῖ: I disagree with the naming style. I find a clear setXxx (or set_xxx) a much stronger signal that the operation is mutable, whereas day(5) could simply be a getter with a parameter.
        – Matthieu M.
        Feb 24 at 18:25






      • 1




        @πάνταῥεῖ: I also disagree with the unsigned advice; nobody wants modulo arithmetic on date components, it's only surprising :(
        – Matthieu M.
        Feb 24 at 18:25






      • 1




        "symbols starting with a prefixed _ are reserved..." Actually not quite right. Symbols starting with _ then a capital letter are reserved (_Capital, _DATE_H_, etc). But if they are _lowercase, they are only reserved in the global namespace; they are not reserved at class scope. I wouldn't do it in a class scope anyway, so I don't have to remember the exact rules, but _member is technically fine.
        – Justin
        Feb 24 at 19:40







      • 2




        @einpoklum I'm still convinced that how all the standard c++ library functions do it is fine.
        – Ï€Î¬Î½Ï„α ῥεῖ
        Feb 25 at 11:55







      1




      1




      @πάνταῥεῖ Just a few things: Point 3: In your proposed static version, I think there's good point about using std::array instead of raw C arrays, especially since you can then compare to arr.size() directly instead of relying on the magic number 12. Point 5: You don't really need to move the underscores to the back here. As far as the standard is concerned, everything beginning with an underscore is reserved only in the global namespace. Only names beginning with a underscore followed by a capital letter or beginning with two underscores are reserved in every context.
      – Ben Steffan
      Feb 24 at 16:41




      @πάνταῥεῖ Just a few things: Point 3: In your proposed static version, I think there's good point about using std::array instead of raw C arrays, especially since you can then compare to arr.size() directly instead of relying on the magic number 12. Point 5: You don't really need to move the underscores to the back here. As far as the standard is concerned, everything beginning with an underscore is reserved only in the global namespace. Only names beginning with a underscore followed by a capital letter or beginning with two underscores are reserved in every context.
      – Ben Steffan
      Feb 24 at 16:41




      1




      1




      @πάνταῥεῖ: I disagree with the naming style. I find a clear setXxx (or set_xxx) a much stronger signal that the operation is mutable, whereas day(5) could simply be a getter with a parameter.
      – Matthieu M.
      Feb 24 at 18:25




      @πάνταῥεῖ: I disagree with the naming style. I find a clear setXxx (or set_xxx) a much stronger signal that the operation is mutable, whereas day(5) could simply be a getter with a parameter.
      – Matthieu M.
      Feb 24 at 18:25




      1




      1




      @πάνταῥεῖ: I also disagree with the unsigned advice; nobody wants modulo arithmetic on date components, it's only surprising :(
      – Matthieu M.
      Feb 24 at 18:25




      @πάνταῥεῖ: I also disagree with the unsigned advice; nobody wants modulo arithmetic on date components, it's only surprising :(
      – Matthieu M.
      Feb 24 at 18:25




      1




      1




      "symbols starting with a prefixed _ are reserved..." Actually not quite right. Symbols starting with _ then a capital letter are reserved (_Capital, _DATE_H_, etc). But if they are _lowercase, they are only reserved in the global namespace; they are not reserved at class scope. I wouldn't do it in a class scope anyway, so I don't have to remember the exact rules, but _member is technically fine.
      – Justin
      Feb 24 at 19:40





      "symbols starting with a prefixed _ are reserved..." Actually not quite right. Symbols starting with _ then a capital letter are reserved (_Capital, _DATE_H_, etc). But if they are _lowercase, they are only reserved in the global namespace; they are not reserved at class scope. I wouldn't do it in a class scope anyway, so I don't have to remember the exact rules, but _member is technically fine.
      – Justin
      Feb 24 at 19:40





      2




      2




      @einpoklum I'm still convinced that how all the standard c++ library functions do it is fine.
      – Ï€Î¬Î½Ï„α ῥεῖ
      Feb 25 at 11:55




      @einpoklum I'm still convinced that how all the standard c++ library functions do it is fine.
      – Ï€Î¬Î½Ï„α ῥεῖ
      Feb 25 at 11:55












      up vote
      8
      down vote













      1. Header Names



      I recommend using .hpp for purely C++ header files, and reserving .h to C-compatible header files.



      2. Header Guards



      As mentioned, you should #pragma once rather than #ifndef.



      If you really wish to use the error prone header guards; then I recommend following the Boost convention for naming: PROJECT_PATH_FILE_INCLUDED which helps prevent clashes in header guard names. It's significantly more work, of course...



      3. Namespace



      By defining symbols in the global namespace, you open your code up to conflicts with C libraries, or other uncouth C++ libraries.



      Instead, decide on a namespace, generally the project name, and wrap any single item you define in this namespace.



      4. Use enum class, and pick an underlying type.



      Any new enum should really be enum class, which introduces a scope for the enumerators rather than injecting them in the surrounding scope.



      Since you are no longer polluting the surrounding namespace, you can without fear pull out Month at namespace level, making it easier for people to name the type.



      I also encourage you to pick a type. By default the compiler will use int, which is... 3 bytes too wide in your case. You can easily use int8_t instead, it is large enough.



      Also, you might as well dispense with pointless abbreviations.



      This gives:



      #include <cstdint>

      namespace mydate
      enum class Month: std::int8_t
      January = 1, February, March, April, May, June,
      July, August, September, October, November, December,
      ;



      As a side bonus, enum class can be forward declared by your users.



      5. Exception should inherit std::exception.



      It's a convention that most everyone follows which allows catching all exceptions and being able to display something with .what() which beats ... hands down.



      6. Public first, please.



      Header files are often peeked at by your clients.



      Therefore, it is a good idea to expose your class interface in the order: public, protected, private. This minimizes the number of irrelevant lines that the user has to skip.



      7. There shall be no global state.



      Global state is a plague on software as it prevents local reasoning, the primary tool you have to understand how a piece of software works.



      It also suffers from data-races, however just slapping thread_local is not a panacea either: it doesn't solve the local reasoning issue and prevents clients from using different threading models such as coroutines.



      In your case, it's even worse, because there is no good default date which will satisfy every user.



      8. Global constants are good.



      But they shall be constants, hence:



      static std::array<std::string, 12> const monthNames;


      Using std::array is preferred. You get some goodies, such as bounds checking, easy extraction of size, etc...



      9. Pick your types.



      int is a default type, but it is not very sensible.



      You already have a Month enum, why is not used for _month?



      I also recommend using a Day type, or at least using std::int8_t again since there's no reason to have a day greater than 127.



      10. Stateless helper functions.



      The leapYear function does not access this, so it need not be a class method.



      You can either propose it as a free-function as part of your interface, or otherwise only declare it in the .cpp file in an anonymous namespace.



      11. Never return a const value.



      It's rather pointless, as the client can copy it anyway.



      You may wish to return a T const& (const-reference). Unfortunately it means that clients get to know that your class contains a T, so that if you ever want to change the internals of your class, clients will break.



      12. Defaults are hard.



      If there is no meaningful default value for your type, consider NOT defining a default constructor.



      This is pretty restrictive, of course, so you may consider instead:



      • defining an invalid value, though this means that (1) your class must now have a isValid method and (2) on each getter your class must check whether it is valid.

      • defining a default value nonetheless, and document it. In this case, it may be a good to pick an unlikely value, such as the 1st January of the year 1, to help clients realize when they forgot to set the value.

      13. Beware typeless interfaces.



      The US format for dates is MM/DD/YYYY, the ISO format is YYYY/MM/DD. When you present a constructor Date(int, int, int), it's very easy for a client to accidentally pass the arguments in the wrong order.



      In this sense, Date(int, Month, int) is a step up, since Americans cannot accidentally use their weird format, however it's still possible to accidentally use ISO instead of DD/MM/YYYY as you intend.



      There are 2 possibilities out of there:



      • Only offer strongly typed arguments: Date(Day, Month, Year),

      • Use named constructors instead.

      The latter is done with static methods:



      static Date from_date_month_year(int day, Month month, int year);


      Note: while it's relatively clear that int day will have a range [1, 31], it's unclear whether int month has a range [0, 11] or [1, 12]; don't tempt the user.



      14. Name your parameters.



      Here, d is probably understood, but in general please attempt to give your parameters full names. Abbreviations are really not necessary in general.



      15. Rule of Zero



      The Rule of Zero states that you should rarely (if ever) define a Copy Constructor, Copy Assignment Operator, Move Constructor, Move Assignment Operator or Destructor.



      It is only necessary on technical resource management classes, such as unique_ptr or vector. If you ever find yourself writing such a class, be sure to make it as minimal as possible; without any business logic.



      16. Be consistent.



      Your interface sometimes use int for month (where it's ambiguous) and sometimes Month. Stick to Month.



      17. Follow conventions.



      The return type of operator++ and operator++(int) is never const-qualified, to allow chaining.



      18. Be sparse in your includes.



      You never use ostream, so you may as well only include <iosfwd> in your header.



      Minimizing the amount of includes in your headers help with compilation times.




      I'll stop here, as that's a lot to fix already.



      Congratulations on writing your Date class :)






      share|improve this answer























      • @Justin: Good point; let me put a meaningful name.
        – Matthieu M.
        Feb 24 at 20:28










      • It's a good name now. I'd probably still write from_dmy, since I tend to dislike names that are that long and dmy is clear enough when talking about dates, but naming is really subjective so it's really just preference at that point
        – Justin
        Feb 24 at 20:37










      • #13 I would recommend year, month, day order as it is the usual convention. Month should have a range of 1 to 12. When someone write a date like 2018-02-25, it is clear that 2 is for February and not March so code should follow that convention.
        – Phil1970
        Feb 25 at 16:15










      • @Phil1970: I would personally recommend multiple named constructors, with varied formats. This is the point of named constructors, after all. I simply limited myself to show to the OP how to rewrite their existing constructor as a static factory method.
        – Matthieu M.
        Feb 25 at 16:19










      • Strongly disagree on point 2, until you can add a reference to the relevant section of a C++ standard that specifies #pragma once.
        – Toby Speight
        Feb 27 at 9:31














      up vote
      8
      down vote













      1. Header Names



      I recommend using .hpp for purely C++ header files, and reserving .h to C-compatible header files.



      2. Header Guards



      As mentioned, you should #pragma once rather than #ifndef.



      If you really wish to use the error prone header guards; then I recommend following the Boost convention for naming: PROJECT_PATH_FILE_INCLUDED which helps prevent clashes in header guard names. It's significantly more work, of course...



      3. Namespace



      By defining symbols in the global namespace, you open your code up to conflicts with C libraries, or other uncouth C++ libraries.



      Instead, decide on a namespace, generally the project name, and wrap any single item you define in this namespace.



      4. Use enum class, and pick an underlying type.



      Any new enum should really be enum class, which introduces a scope for the enumerators rather than injecting them in the surrounding scope.



      Since you are no longer polluting the surrounding namespace, you can without fear pull out Month at namespace level, making it easier for people to name the type.



      I also encourage you to pick a type. By default the compiler will use int, which is... 3 bytes too wide in your case. You can easily use int8_t instead, it is large enough.



      Also, you might as well dispense with pointless abbreviations.



      This gives:



      #include <cstdint>

      namespace mydate
      enum class Month: std::int8_t
      January = 1, February, March, April, May, June,
      July, August, September, October, November, December,
      ;



      As a side bonus, enum class can be forward declared by your users.



      5. Exception should inherit std::exception.



      It's a convention that most everyone follows which allows catching all exceptions and being able to display something with .what() which beats ... hands down.



      6. Public first, please.



      Header files are often peeked at by your clients.



      Therefore, it is a good idea to expose your class interface in the order: public, protected, private. This minimizes the number of irrelevant lines that the user has to skip.



      7. There shall be no global state.



      Global state is a plague on software as it prevents local reasoning, the primary tool you have to understand how a piece of software works.



      It also suffers from data-races, however just slapping thread_local is not a panacea either: it doesn't solve the local reasoning issue and prevents clients from using different threading models such as coroutines.



      In your case, it's even worse, because there is no good default date which will satisfy every user.



      8. Global constants are good.



      But they shall be constants, hence:



      static std::array<std::string, 12> const monthNames;


      Using std::array is preferred. You get some goodies, such as bounds checking, easy extraction of size, etc...



      9. Pick your types.



      int is a default type, but it is not very sensible.



      You already have a Month enum, why is not used for _month?



      I also recommend using a Day type, or at least using std::int8_t again since there's no reason to have a day greater than 127.



      10. Stateless helper functions.



      The leapYear function does not access this, so it need not be a class method.



      You can either propose it as a free-function as part of your interface, or otherwise only declare it in the .cpp file in an anonymous namespace.



      11. Never return a const value.



      It's rather pointless, as the client can copy it anyway.



      You may wish to return a T const& (const-reference). Unfortunately it means that clients get to know that your class contains a T, so that if you ever want to change the internals of your class, clients will break.



      12. Defaults are hard.



      If there is no meaningful default value for your type, consider NOT defining a default constructor.



      This is pretty restrictive, of course, so you may consider instead:



      • defining an invalid value, though this means that (1) your class must now have a isValid method and (2) on each getter your class must check whether it is valid.

      • defining a default value nonetheless, and document it. In this case, it may be a good to pick an unlikely value, such as the 1st January of the year 1, to help clients realize when they forgot to set the value.

      13. Beware typeless interfaces.



      The US format for dates is MM/DD/YYYY, the ISO format is YYYY/MM/DD. When you present a constructor Date(int, int, int), it's very easy for a client to accidentally pass the arguments in the wrong order.



      In this sense, Date(int, Month, int) is a step up, since Americans cannot accidentally use their weird format, however it's still possible to accidentally use ISO instead of DD/MM/YYYY as you intend.



      There are 2 possibilities out of there:



      • Only offer strongly typed arguments: Date(Day, Month, Year),

      • Use named constructors instead.

      The latter is done with static methods:



      static Date from_date_month_year(int day, Month month, int year);


      Note: while it's relatively clear that int day will have a range [1, 31], it's unclear whether int month has a range [0, 11] or [1, 12]; don't tempt the user.



      14. Name your parameters.



      Here, d is probably understood, but in general please attempt to give your parameters full names. Abbreviations are really not necessary in general.



      15. Rule of Zero



      The Rule of Zero states that you should rarely (if ever) define a Copy Constructor, Copy Assignment Operator, Move Constructor, Move Assignment Operator or Destructor.



      It is only necessary on technical resource management classes, such as unique_ptr or vector. If you ever find yourself writing such a class, be sure to make it as minimal as possible; without any business logic.



      16. Be consistent.



      Your interface sometimes use int for month (where it's ambiguous) and sometimes Month. Stick to Month.



      17. Follow conventions.



      The return type of operator++ and operator++(int) is never const-qualified, to allow chaining.



      18. Be sparse in your includes.



      You never use ostream, so you may as well only include <iosfwd> in your header.



      Minimizing the amount of includes in your headers help with compilation times.




      I'll stop here, as that's a lot to fix already.



      Congratulations on writing your Date class :)






      share|improve this answer























      • @Justin: Good point; let me put a meaningful name.
        – Matthieu M.
        Feb 24 at 20:28










      • It's a good name now. I'd probably still write from_dmy, since I tend to dislike names that are that long and dmy is clear enough when talking about dates, but naming is really subjective so it's really just preference at that point
        – Justin
        Feb 24 at 20:37










      • #13 I would recommend year, month, day order as it is the usual convention. Month should have a range of 1 to 12. When someone write a date like 2018-02-25, it is clear that 2 is for February and not March so code should follow that convention.
        – Phil1970
        Feb 25 at 16:15










      • @Phil1970: I would personally recommend multiple named constructors, with varied formats. This is the point of named constructors, after all. I simply limited myself to show to the OP how to rewrite their existing constructor as a static factory method.
        – Matthieu M.
        Feb 25 at 16:19










      • Strongly disagree on point 2, until you can add a reference to the relevant section of a C++ standard that specifies #pragma once.
        – Toby Speight
        Feb 27 at 9:31












      up vote
      8
      down vote










      up vote
      8
      down vote









      1. Header Names



      I recommend using .hpp for purely C++ header files, and reserving .h to C-compatible header files.



      2. Header Guards



      As mentioned, you should #pragma once rather than #ifndef.



      If you really wish to use the error prone header guards; then I recommend following the Boost convention for naming: PROJECT_PATH_FILE_INCLUDED which helps prevent clashes in header guard names. It's significantly more work, of course...



      3. Namespace



      By defining symbols in the global namespace, you open your code up to conflicts with C libraries, or other uncouth C++ libraries.



      Instead, decide on a namespace, generally the project name, and wrap any single item you define in this namespace.



      4. Use enum class, and pick an underlying type.



      Any new enum should really be enum class, which introduces a scope for the enumerators rather than injecting them in the surrounding scope.



      Since you are no longer polluting the surrounding namespace, you can without fear pull out Month at namespace level, making it easier for people to name the type.



      I also encourage you to pick a type. By default the compiler will use int, which is... 3 bytes too wide in your case. You can easily use int8_t instead, it is large enough.



      Also, you might as well dispense with pointless abbreviations.



      This gives:



      #include <cstdint>

      namespace mydate
      enum class Month: std::int8_t
      January = 1, February, March, April, May, June,
      July, August, September, October, November, December,
      ;



      As a side bonus, enum class can be forward declared by your users.



      5. Exception should inherit std::exception.



      It's a convention that most everyone follows which allows catching all exceptions and being able to display something with .what() which beats ... hands down.



      6. Public first, please.



      Header files are often peeked at by your clients.



      Therefore, it is a good idea to expose your class interface in the order: public, protected, private. This minimizes the number of irrelevant lines that the user has to skip.



      7. There shall be no global state.



      Global state is a plague on software as it prevents local reasoning, the primary tool you have to understand how a piece of software works.



      It also suffers from data-races, however just slapping thread_local is not a panacea either: it doesn't solve the local reasoning issue and prevents clients from using different threading models such as coroutines.



      In your case, it's even worse, because there is no good default date which will satisfy every user.



      8. Global constants are good.



      But they shall be constants, hence:



      static std::array<std::string, 12> const monthNames;


      Using std::array is preferred. You get some goodies, such as bounds checking, easy extraction of size, etc...



      9. Pick your types.



      int is a default type, but it is not very sensible.



      You already have a Month enum, why is not used for _month?



      I also recommend using a Day type, or at least using std::int8_t again since there's no reason to have a day greater than 127.



      10. Stateless helper functions.



      The leapYear function does not access this, so it need not be a class method.



      You can either propose it as a free-function as part of your interface, or otherwise only declare it in the .cpp file in an anonymous namespace.



      11. Never return a const value.



      It's rather pointless, as the client can copy it anyway.



      You may wish to return a T const& (const-reference). Unfortunately it means that clients get to know that your class contains a T, so that if you ever want to change the internals of your class, clients will break.



      12. Defaults are hard.



      If there is no meaningful default value for your type, consider NOT defining a default constructor.



      This is pretty restrictive, of course, so you may consider instead:



      • defining an invalid value, though this means that (1) your class must now have a isValid method and (2) on each getter your class must check whether it is valid.

      • defining a default value nonetheless, and document it. In this case, it may be a good to pick an unlikely value, such as the 1st January of the year 1, to help clients realize when they forgot to set the value.

      13. Beware typeless interfaces.



      The US format for dates is MM/DD/YYYY, the ISO format is YYYY/MM/DD. When you present a constructor Date(int, int, int), it's very easy for a client to accidentally pass the arguments in the wrong order.



      In this sense, Date(int, Month, int) is a step up, since Americans cannot accidentally use their weird format, however it's still possible to accidentally use ISO instead of DD/MM/YYYY as you intend.



      There are 2 possibilities out of there:



      • Only offer strongly typed arguments: Date(Day, Month, Year),

      • Use named constructors instead.

      The latter is done with static methods:



      static Date from_date_month_year(int day, Month month, int year);


      Note: while it's relatively clear that int day will have a range [1, 31], it's unclear whether int month has a range [0, 11] or [1, 12]; don't tempt the user.



      14. Name your parameters.



      Here, d is probably understood, but in general please attempt to give your parameters full names. Abbreviations are really not necessary in general.



      15. Rule of Zero



      The Rule of Zero states that you should rarely (if ever) define a Copy Constructor, Copy Assignment Operator, Move Constructor, Move Assignment Operator or Destructor.



      It is only necessary on technical resource management classes, such as unique_ptr or vector. If you ever find yourself writing such a class, be sure to make it as minimal as possible; without any business logic.



      16. Be consistent.



      Your interface sometimes use int for month (where it's ambiguous) and sometimes Month. Stick to Month.



      17. Follow conventions.



      The return type of operator++ and operator++(int) is never const-qualified, to allow chaining.



      18. Be sparse in your includes.



      You never use ostream, so you may as well only include <iosfwd> in your header.



      Minimizing the amount of includes in your headers help with compilation times.




      I'll stop here, as that's a lot to fix already.



      Congratulations on writing your Date class :)






      share|improve this answer















      1. Header Names



      I recommend using .hpp for purely C++ header files, and reserving .h to C-compatible header files.



      2. Header Guards



      As mentioned, you should #pragma once rather than #ifndef.



      If you really wish to use the error prone header guards; then I recommend following the Boost convention for naming: PROJECT_PATH_FILE_INCLUDED which helps prevent clashes in header guard names. It's significantly more work, of course...



      3. Namespace



      By defining symbols in the global namespace, you open your code up to conflicts with C libraries, or other uncouth C++ libraries.



      Instead, decide on a namespace, generally the project name, and wrap any single item you define in this namespace.



      4. Use enum class, and pick an underlying type.



      Any new enum should really be enum class, which introduces a scope for the enumerators rather than injecting them in the surrounding scope.



      Since you are no longer polluting the surrounding namespace, you can without fear pull out Month at namespace level, making it easier for people to name the type.



      I also encourage you to pick a type. By default the compiler will use int, which is... 3 bytes too wide in your case. You can easily use int8_t instead, it is large enough.



      Also, you might as well dispense with pointless abbreviations.



      This gives:



      #include <cstdint>

      namespace mydate
      enum class Month: std::int8_t
      January = 1, February, March, April, May, June,
      July, August, September, October, November, December,
      ;



      As a side bonus, enum class can be forward declared by your users.



      5. Exception should inherit std::exception.



      It's a convention that most everyone follows which allows catching all exceptions and being able to display something with .what() which beats ... hands down.



      6. Public first, please.



      Header files are often peeked at by your clients.



      Therefore, it is a good idea to expose your class interface in the order: public, protected, private. This minimizes the number of irrelevant lines that the user has to skip.



      7. There shall be no global state.



      Global state is a plague on software as it prevents local reasoning, the primary tool you have to understand how a piece of software works.



      It also suffers from data-races, however just slapping thread_local is not a panacea either: it doesn't solve the local reasoning issue and prevents clients from using different threading models such as coroutines.



      In your case, it's even worse, because there is no good default date which will satisfy every user.



      8. Global constants are good.



      But they shall be constants, hence:



      static std::array<std::string, 12> const monthNames;


      Using std::array is preferred. You get some goodies, such as bounds checking, easy extraction of size, etc...



      9. Pick your types.



      int is a default type, but it is not very sensible.



      You already have a Month enum, why is not used for _month?



      I also recommend using a Day type, or at least using std::int8_t again since there's no reason to have a day greater than 127.



      10. Stateless helper functions.



      The leapYear function does not access this, so it need not be a class method.



      You can either propose it as a free-function as part of your interface, or otherwise only declare it in the .cpp file in an anonymous namespace.



      11. Never return a const value.



      It's rather pointless, as the client can copy it anyway.



      You may wish to return a T const& (const-reference). Unfortunately it means that clients get to know that your class contains a T, so that if you ever want to change the internals of your class, clients will break.



      12. Defaults are hard.



      If there is no meaningful default value for your type, consider NOT defining a default constructor.



      This is pretty restrictive, of course, so you may consider instead:



      • defining an invalid value, though this means that (1) your class must now have a isValid method and (2) on each getter your class must check whether it is valid.

      • defining a default value nonetheless, and document it. In this case, it may be a good to pick an unlikely value, such as the 1st January of the year 1, to help clients realize when they forgot to set the value.

      13. Beware typeless interfaces.



      The US format for dates is MM/DD/YYYY, the ISO format is YYYY/MM/DD. When you present a constructor Date(int, int, int), it's very easy for a client to accidentally pass the arguments in the wrong order.



      In this sense, Date(int, Month, int) is a step up, since Americans cannot accidentally use their weird format, however it's still possible to accidentally use ISO instead of DD/MM/YYYY as you intend.



      There are 2 possibilities out of there:



      • Only offer strongly typed arguments: Date(Day, Month, Year),

      • Use named constructors instead.

      The latter is done with static methods:



      static Date from_date_month_year(int day, Month month, int year);


      Note: while it's relatively clear that int day will have a range [1, 31], it's unclear whether int month has a range [0, 11] or [1, 12]; don't tempt the user.



      14. Name your parameters.



      Here, d is probably understood, but in general please attempt to give your parameters full names. Abbreviations are really not necessary in general.



      15. Rule of Zero



      The Rule of Zero states that you should rarely (if ever) define a Copy Constructor, Copy Assignment Operator, Move Constructor, Move Assignment Operator or Destructor.



      It is only necessary on technical resource management classes, such as unique_ptr or vector. If you ever find yourself writing such a class, be sure to make it as minimal as possible; without any business logic.



      16. Be consistent.



      Your interface sometimes use int for month (where it's ambiguous) and sometimes Month. Stick to Month.



      17. Follow conventions.



      The return type of operator++ and operator++(int) is never const-qualified, to allow chaining.



      18. Be sparse in your includes.



      You never use ostream, so you may as well only include <iosfwd> in your header.



      Minimizing the amount of includes in your headers help with compilation times.




      I'll stop here, as that's a lot to fix already.



      Congratulations on writing your Date class :)







      share|improve this answer















      share|improve this answer



      share|improve this answer








      edited Feb 24 at 20:28


























      answered Feb 24 at 19:12









      Matthieu M.

      2,0821710




      2,0821710











      • @Justin: Good point; let me put a meaningful name.
        – Matthieu M.
        Feb 24 at 20:28










      • It's a good name now. I'd probably still write from_dmy, since I tend to dislike names that are that long and dmy is clear enough when talking about dates, but naming is really subjective so it's really just preference at that point
        – Justin
        Feb 24 at 20:37










      • #13 I would recommend year, month, day order as it is the usual convention. Month should have a range of 1 to 12. When someone write a date like 2018-02-25, it is clear that 2 is for February and not March so code should follow that convention.
        – Phil1970
        Feb 25 at 16:15










      • @Phil1970: I would personally recommend multiple named constructors, with varied formats. This is the point of named constructors, after all. I simply limited myself to show to the OP how to rewrite their existing constructor as a static factory method.
        – Matthieu M.
        Feb 25 at 16:19










      • Strongly disagree on point 2, until you can add a reference to the relevant section of a C++ standard that specifies #pragma once.
        – Toby Speight
        Feb 27 at 9:31
















      • @Justin: Good point; let me put a meaningful name.
        – Matthieu M.
        Feb 24 at 20:28










      • It's a good name now. I'd probably still write from_dmy, since I tend to dislike names that are that long and dmy is clear enough when talking about dates, but naming is really subjective so it's really just preference at that point
        – Justin
        Feb 24 at 20:37










      • #13 I would recommend year, month, day order as it is the usual convention. Month should have a range of 1 to 12. When someone write a date like 2018-02-25, it is clear that 2 is for February and not March so code should follow that convention.
        – Phil1970
        Feb 25 at 16:15










      • @Phil1970: I would personally recommend multiple named constructors, with varied formats. This is the point of named constructors, after all. I simply limited myself to show to the OP how to rewrite their existing constructor as a static factory method.
        – Matthieu M.
        Feb 25 at 16:19










      • Strongly disagree on point 2, until you can add a reference to the relevant section of a C++ standard that specifies #pragma once.
        – Toby Speight
        Feb 27 at 9:31















      @Justin: Good point; let me put a meaningful name.
      – Matthieu M.
      Feb 24 at 20:28




      @Justin: Good point; let me put a meaningful name.
      – Matthieu M.
      Feb 24 at 20:28












      It's a good name now. I'd probably still write from_dmy, since I tend to dislike names that are that long and dmy is clear enough when talking about dates, but naming is really subjective so it's really just preference at that point
      – Justin
      Feb 24 at 20:37




      It's a good name now. I'd probably still write from_dmy, since I tend to dislike names that are that long and dmy is clear enough when talking about dates, but naming is really subjective so it's really just preference at that point
      – Justin
      Feb 24 at 20:37












      #13 I would recommend year, month, day order as it is the usual convention. Month should have a range of 1 to 12. When someone write a date like 2018-02-25, it is clear that 2 is for February and not March so code should follow that convention.
      – Phil1970
      Feb 25 at 16:15




      #13 I would recommend year, month, day order as it is the usual convention. Month should have a range of 1 to 12. When someone write a date like 2018-02-25, it is clear that 2 is for February and not March so code should follow that convention.
      – Phil1970
      Feb 25 at 16:15












      @Phil1970: I would personally recommend multiple named constructors, with varied formats. This is the point of named constructors, after all. I simply limited myself to show to the OP how to rewrite their existing constructor as a static factory method.
      – Matthieu M.
      Feb 25 at 16:19




      @Phil1970: I would personally recommend multiple named constructors, with varied formats. This is the point of named constructors, after all. I simply limited myself to show to the OP how to rewrite their existing constructor as a static factory method.
      – Matthieu M.
      Feb 25 at 16:19












      Strongly disagree on point 2, until you can add a reference to the relevant section of a C++ standard that specifies #pragma once.
      – Toby Speight
      Feb 27 at 9:31




      Strongly disagree on point 2, until you can add a reference to the relevant section of a C++ standard that specifies #pragma once.
      – Toby Speight
      Feb 27 at 9:31










      up vote
      6
      down vote













      [code] header guard



      Personally I'd recommend just using #pragma once. It's supported by most compilers, it's less typing, less work for the preprocessor, and much less error prone (renaming files and forgetting to rename the define, issues with reserved names, typos in the definition / check, etc.)



      [design] default



      The static default functionality probably shouldn't be inside the class as it's error prone and assumes the users needs (e.g. What if they need two different defaults? They'd have to keep changing it backwards and forwards, which makes it pointless).



      If the user wants a default value, they can store a constant Date instance somewhere themselves, and pass that it into the constructor easily enough. This also makes it instantly obvious what value an intialized object is:



      const Date defaultDate(23, Date::Month::mar, 3);

      Date d1; // no idea what value this has... it depends what the default is at the moment! we now have to find out if this is set anywhere else in the codebase, AND then understand which code paths lead from there to here...
      Date d2(defaultDate); // it's set to the value of defaultDate!


      [design] checking values



      Presumably, the idea is to prevent creation of an invalid date, and have a transactional interface (i.e. changing the date either works, or throws an exception, leaving the current value untouched.



      (Bug:) However, the way fillDate is currently defined, we try to set each component individually, then check if the whole date object is still valid at each step. This may cause problems, because an intermediate date may be valid (and so we successfully change the day or month), but not the final date. This means that although we throw an error, the initial date value has still been changed.



      Furthermore, setting a value, then checking, then setting it back again and throwing is inefficient.



      Instead we could just pass the numbers to be checked to a checking function (i.e. isIllFormed) before setting anything. This can also simplify construction and assignment:



      Date::Date(): // default constructor provided
      _day(1), _month(1), _year(2000)

      Date::Date(int d, int m, int y)
      : Date() // use delegating constructor then call a single point of checking / setting

      set(d, m, y);


      Date::Date(const Date& that)
      : Date()

      set(that.day(), that.month(), that.year());


      Date& Date::operator=(const Date& that)

      set(that.day(), that.month(), that.year());

      return *this;


      void Date::set(int day, int month, int year)

      if (!isValidDate(day, month, year)) // this check and the throw could be perhaps abstracted into a throwIfInvalid function...
      throw BadDate(day, month, year);

      _day = day;
      _month = month;
      _year = year;


      void Date::setDay(const int d)

      if (!isValidDate(d, month(), year()))
      throw BadDate(d, month(), year());

      _day = d;

      void Date::setMonth(const int m)

      if (!isValidDate(day(), m, year()))
      throw BadDate(day(), m, year());

      _month = m;

      void Date::setYear(const int y)

      if (!isValidDate(day(), month(), y))
      throw BadDate(day(), month(), y);

      _year = y;


      // isValidDate is just isIllFormed with the logic reversed


      isValidDate and isLeapYear can be static functions, and might as well be public, since they'd be useful for users who need to validate input before creating a date class:



      static bool isValidDate(int d, int m, int y);
      static bool isLeapYear(int y);





      share|improve this answer

























        up vote
        6
        down vote













        [code] header guard



        Personally I'd recommend just using #pragma once. It's supported by most compilers, it's less typing, less work for the preprocessor, and much less error prone (renaming files and forgetting to rename the define, issues with reserved names, typos in the definition / check, etc.)



        [design] default



        The static default functionality probably shouldn't be inside the class as it's error prone and assumes the users needs (e.g. What if they need two different defaults? They'd have to keep changing it backwards and forwards, which makes it pointless).



        If the user wants a default value, they can store a constant Date instance somewhere themselves, and pass that it into the constructor easily enough. This also makes it instantly obvious what value an intialized object is:



        const Date defaultDate(23, Date::Month::mar, 3);

        Date d1; // no idea what value this has... it depends what the default is at the moment! we now have to find out if this is set anywhere else in the codebase, AND then understand which code paths lead from there to here...
        Date d2(defaultDate); // it's set to the value of defaultDate!


        [design] checking values



        Presumably, the idea is to prevent creation of an invalid date, and have a transactional interface (i.e. changing the date either works, or throws an exception, leaving the current value untouched.



        (Bug:) However, the way fillDate is currently defined, we try to set each component individually, then check if the whole date object is still valid at each step. This may cause problems, because an intermediate date may be valid (and so we successfully change the day or month), but not the final date. This means that although we throw an error, the initial date value has still been changed.



        Furthermore, setting a value, then checking, then setting it back again and throwing is inefficient.



        Instead we could just pass the numbers to be checked to a checking function (i.e. isIllFormed) before setting anything. This can also simplify construction and assignment:



        Date::Date(): // default constructor provided
        _day(1), _month(1), _year(2000)

        Date::Date(int d, int m, int y)
        : Date() // use delegating constructor then call a single point of checking / setting

        set(d, m, y);


        Date::Date(const Date& that)
        : Date()

        set(that.day(), that.month(), that.year());


        Date& Date::operator=(const Date& that)

        set(that.day(), that.month(), that.year());

        return *this;


        void Date::set(int day, int month, int year)

        if (!isValidDate(day, month, year)) // this check and the throw could be perhaps abstracted into a throwIfInvalid function...
        throw BadDate(day, month, year);

        _day = day;
        _month = month;
        _year = year;


        void Date::setDay(const int d)

        if (!isValidDate(d, month(), year()))
        throw BadDate(d, month(), year());

        _day = d;

        void Date::setMonth(const int m)

        if (!isValidDate(day(), m, year()))
        throw BadDate(day(), m, year());

        _month = m;

        void Date::setYear(const int y)

        if (!isValidDate(day(), month(), y))
        throw BadDate(day(), month(), y);

        _year = y;


        // isValidDate is just isIllFormed with the logic reversed


        isValidDate and isLeapYear can be static functions, and might as well be public, since they'd be useful for users who need to validate input before creating a date class:



        static bool isValidDate(int d, int m, int y);
        static bool isLeapYear(int y);





        share|improve this answer























          up vote
          6
          down vote










          up vote
          6
          down vote









          [code] header guard



          Personally I'd recommend just using #pragma once. It's supported by most compilers, it's less typing, less work for the preprocessor, and much less error prone (renaming files and forgetting to rename the define, issues with reserved names, typos in the definition / check, etc.)



          [design] default



          The static default functionality probably shouldn't be inside the class as it's error prone and assumes the users needs (e.g. What if they need two different defaults? They'd have to keep changing it backwards and forwards, which makes it pointless).



          If the user wants a default value, they can store a constant Date instance somewhere themselves, and pass that it into the constructor easily enough. This also makes it instantly obvious what value an intialized object is:



          const Date defaultDate(23, Date::Month::mar, 3);

          Date d1; // no idea what value this has... it depends what the default is at the moment! we now have to find out if this is set anywhere else in the codebase, AND then understand which code paths lead from there to here...
          Date d2(defaultDate); // it's set to the value of defaultDate!


          [design] checking values



          Presumably, the idea is to prevent creation of an invalid date, and have a transactional interface (i.e. changing the date either works, or throws an exception, leaving the current value untouched.



          (Bug:) However, the way fillDate is currently defined, we try to set each component individually, then check if the whole date object is still valid at each step. This may cause problems, because an intermediate date may be valid (and so we successfully change the day or month), but not the final date. This means that although we throw an error, the initial date value has still been changed.



          Furthermore, setting a value, then checking, then setting it back again and throwing is inefficient.



          Instead we could just pass the numbers to be checked to a checking function (i.e. isIllFormed) before setting anything. This can also simplify construction and assignment:



          Date::Date(): // default constructor provided
          _day(1), _month(1), _year(2000)

          Date::Date(int d, int m, int y)
          : Date() // use delegating constructor then call a single point of checking / setting

          set(d, m, y);


          Date::Date(const Date& that)
          : Date()

          set(that.day(), that.month(), that.year());


          Date& Date::operator=(const Date& that)

          set(that.day(), that.month(), that.year());

          return *this;


          void Date::set(int day, int month, int year)

          if (!isValidDate(day, month, year)) // this check and the throw could be perhaps abstracted into a throwIfInvalid function...
          throw BadDate(day, month, year);

          _day = day;
          _month = month;
          _year = year;


          void Date::setDay(const int d)

          if (!isValidDate(d, month(), year()))
          throw BadDate(d, month(), year());

          _day = d;

          void Date::setMonth(const int m)

          if (!isValidDate(day(), m, year()))
          throw BadDate(day(), m, year());

          _month = m;

          void Date::setYear(const int y)

          if (!isValidDate(day(), month(), y))
          throw BadDate(day(), month(), y);

          _year = y;


          // isValidDate is just isIllFormed with the logic reversed


          isValidDate and isLeapYear can be static functions, and might as well be public, since they'd be useful for users who need to validate input before creating a date class:



          static bool isValidDate(int d, int m, int y);
          static bool isLeapYear(int y);





          share|improve this answer













          [code] header guard



          Personally I'd recommend just using #pragma once. It's supported by most compilers, it's less typing, less work for the preprocessor, and much less error prone (renaming files and forgetting to rename the define, issues with reserved names, typos in the definition / check, etc.)



          [design] default



          The static default functionality probably shouldn't be inside the class as it's error prone and assumes the users needs (e.g. What if they need two different defaults? They'd have to keep changing it backwards and forwards, which makes it pointless).



          If the user wants a default value, they can store a constant Date instance somewhere themselves, and pass that it into the constructor easily enough. This also makes it instantly obvious what value an intialized object is:



          const Date defaultDate(23, Date::Month::mar, 3);

          Date d1; // no idea what value this has... it depends what the default is at the moment! we now have to find out if this is set anywhere else in the codebase, AND then understand which code paths lead from there to here...
          Date d2(defaultDate); // it's set to the value of defaultDate!


          [design] checking values



          Presumably, the idea is to prevent creation of an invalid date, and have a transactional interface (i.e. changing the date either works, or throws an exception, leaving the current value untouched.



          (Bug:) However, the way fillDate is currently defined, we try to set each component individually, then check if the whole date object is still valid at each step. This may cause problems, because an intermediate date may be valid (and so we successfully change the day or month), but not the final date. This means that although we throw an error, the initial date value has still been changed.



          Furthermore, setting a value, then checking, then setting it back again and throwing is inefficient.



          Instead we could just pass the numbers to be checked to a checking function (i.e. isIllFormed) before setting anything. This can also simplify construction and assignment:



          Date::Date(): // default constructor provided
          _day(1), _month(1), _year(2000)

          Date::Date(int d, int m, int y)
          : Date() // use delegating constructor then call a single point of checking / setting

          set(d, m, y);


          Date::Date(const Date& that)
          : Date()

          set(that.day(), that.month(), that.year());


          Date& Date::operator=(const Date& that)

          set(that.day(), that.month(), that.year());

          return *this;


          void Date::set(int day, int month, int year)

          if (!isValidDate(day, month, year)) // this check and the throw could be perhaps abstracted into a throwIfInvalid function...
          throw BadDate(day, month, year);

          _day = day;
          _month = month;
          _year = year;


          void Date::setDay(const int d)

          if (!isValidDate(d, month(), year()))
          throw BadDate(d, month(), year());

          _day = d;

          void Date::setMonth(const int m)

          if (!isValidDate(day(), m, year()))
          throw BadDate(day(), m, year());

          _month = m;

          void Date::setYear(const int y)

          if (!isValidDate(day(), month(), y))
          throw BadDate(day(), month(), y);

          _year = y;


          // isValidDate is just isIllFormed with the logic reversed


          isValidDate and isLeapYear can be static functions, and might as well be public, since they'd be useful for users who need to validate input before creating a date class:



          static bool isValidDate(int d, int m, int y);
          static bool isLeapYear(int y);






          share|improve this answer













          share|improve this answer



          share|improve this answer











          answered Feb 24 at 13:22









          user673679

          1,042518




          1,042518




















              up vote
              2
              down vote













              In addition to the many points raised in the 3 existing answers, there are still a lot of other issues in that design.



              Assignment



              1. Operator = is inefficient. If validation was properly done with exception so that it is impossible to create an invalid date, then you won't need validation… And even if you want validation, you don't need to do it 3 times.


              2. Individual functions like setDay, setMonth and setYear does not make much sense for a date. You almost always want to set all 3 components at once. And for the very few times you would want to update only some components, you can always read parts that do no not change.


              Construction



              1. Constructor order is unusual. Almost everyone would assume that the order is year, month and day and doing otherwise would only cause many bugs.


              2. Your customizable default value does not make much sense. Also, it is not thread safe. If you need some predefined values, you make some constants. Default date would usually be the minimum supported date (for ex. 0001-01-01).


              3. Also, you should not have default values for each parameter on the constructor taking an int, a Month and another int. The default would very rarely be appropriate so if someone create a date with some defaults, then the code would be harder to understand. Also as your constructor is not explicit, it can lead to undesirable conversion constructor being called in some contexts.


              Increment and decrement operators



              1. Your postfix operators do not returns the old value as expected. As already mentioned by others, return type should not be const. You should almost always mimic what is done on predefined types.


              2. Your operators does not properly handle month and year changes. If you provide such operators, then at least you should make sure that incrementing last day of a month will return first day of next month. And for decrementing, you should go to last day of previous month. And you probably also want to ensure that your date stays within the supported range.



              3. It is recommended to code the postfix operator using the prefix operator to avoid code duplication. You can easily write something like:



                Date operator++(int)

                Date result(*this); // Make a copy
                ++*this; // Increment this object
                return result; // Return old value



              Readability



              1. Use spacing in expression. Your should have a space around every binary operator in an expression (and also after branching keywords). So for ex. if(y%4) should really be if (y % 4).


              2. You should avoid short variable names like d. By writing day instead, the code is much easier to read.


              3. You should not write void for empty parameter lists in C++ like you did in: Date::~Date(void). Even worse, your definition is inconsistent with your declaration.


              Validation issues



              1. If you try to modify multiple parts of a date, like going from 2017-03-31 to 2016-02-29, it would fail if you do validation after setting the day and month but not the year.


              2. You should also use assertion in your code to verify that you get expected results or that the code is properly used.


              Unit testing



              1. Before writing a library like this, you should write unit tests. Many problems like incorrect postfix operator result can easily be detected with a properly written test.

              Conclusion



              Given the high number of issues (between 30 and 45 issues), I would recommend you to read a bunch of good C++ books before writing more "library" code. I highly recommend Meyers and Sutter books among others.



              Also other libraries could be used as a source of inspiration even in other language like DateTime struct in .NET. In that case, value is immutable which is an even more valuable design.






              share|improve this answer



























                up vote
                2
                down vote













                In addition to the many points raised in the 3 existing answers, there are still a lot of other issues in that design.



                Assignment



                1. Operator = is inefficient. If validation was properly done with exception so that it is impossible to create an invalid date, then you won't need validation… And even if you want validation, you don't need to do it 3 times.


                2. Individual functions like setDay, setMonth and setYear does not make much sense for a date. You almost always want to set all 3 components at once. And for the very few times you would want to update only some components, you can always read parts that do no not change.


                Construction



                1. Constructor order is unusual. Almost everyone would assume that the order is year, month and day and doing otherwise would only cause many bugs.


                2. Your customizable default value does not make much sense. Also, it is not thread safe. If you need some predefined values, you make some constants. Default date would usually be the minimum supported date (for ex. 0001-01-01).


                3. Also, you should not have default values for each parameter on the constructor taking an int, a Month and another int. The default would very rarely be appropriate so if someone create a date with some defaults, then the code would be harder to understand. Also as your constructor is not explicit, it can lead to undesirable conversion constructor being called in some contexts.


                Increment and decrement operators



                1. Your postfix operators do not returns the old value as expected. As already mentioned by others, return type should not be const. You should almost always mimic what is done on predefined types.


                2. Your operators does not properly handle month and year changes. If you provide such operators, then at least you should make sure that incrementing last day of a month will return first day of next month. And for decrementing, you should go to last day of previous month. And you probably also want to ensure that your date stays within the supported range.



                3. It is recommended to code the postfix operator using the prefix operator to avoid code duplication. You can easily write something like:



                  Date operator++(int)

                  Date result(*this); // Make a copy
                  ++*this; // Increment this object
                  return result; // Return old value



                Readability



                1. Use spacing in expression. Your should have a space around every binary operator in an expression (and also after branching keywords). So for ex. if(y%4) should really be if (y % 4).


                2. You should avoid short variable names like d. By writing day instead, the code is much easier to read.


                3. You should not write void for empty parameter lists in C++ like you did in: Date::~Date(void). Even worse, your definition is inconsistent with your declaration.


                Validation issues



                1. If you try to modify multiple parts of a date, like going from 2017-03-31 to 2016-02-29, it would fail if you do validation after setting the day and month but not the year.


                2. You should also use assertion in your code to verify that you get expected results or that the code is properly used.


                Unit testing



                1. Before writing a library like this, you should write unit tests. Many problems like incorrect postfix operator result can easily be detected with a properly written test.

                Conclusion



                Given the high number of issues (between 30 and 45 issues), I would recommend you to read a bunch of good C++ books before writing more "library" code. I highly recommend Meyers and Sutter books among others.



                Also other libraries could be used as a source of inspiration even in other language like DateTime struct in .NET. In that case, value is immutable which is an even more valuable design.






                share|improve this answer

























                  up vote
                  2
                  down vote










                  up vote
                  2
                  down vote









                  In addition to the many points raised in the 3 existing answers, there are still a lot of other issues in that design.



                  Assignment



                  1. Operator = is inefficient. If validation was properly done with exception so that it is impossible to create an invalid date, then you won't need validation… And even if you want validation, you don't need to do it 3 times.


                  2. Individual functions like setDay, setMonth and setYear does not make much sense for a date. You almost always want to set all 3 components at once. And for the very few times you would want to update only some components, you can always read parts that do no not change.


                  Construction



                  1. Constructor order is unusual. Almost everyone would assume that the order is year, month and day and doing otherwise would only cause many bugs.


                  2. Your customizable default value does not make much sense. Also, it is not thread safe. If you need some predefined values, you make some constants. Default date would usually be the minimum supported date (for ex. 0001-01-01).


                  3. Also, you should not have default values for each parameter on the constructor taking an int, a Month and another int. The default would very rarely be appropriate so if someone create a date with some defaults, then the code would be harder to understand. Also as your constructor is not explicit, it can lead to undesirable conversion constructor being called in some contexts.


                  Increment and decrement operators



                  1. Your postfix operators do not returns the old value as expected. As already mentioned by others, return type should not be const. You should almost always mimic what is done on predefined types.


                  2. Your operators does not properly handle month and year changes. If you provide such operators, then at least you should make sure that incrementing last day of a month will return first day of next month. And for decrementing, you should go to last day of previous month. And you probably also want to ensure that your date stays within the supported range.



                  3. It is recommended to code the postfix operator using the prefix operator to avoid code duplication. You can easily write something like:



                    Date operator++(int)

                    Date result(*this); // Make a copy
                    ++*this; // Increment this object
                    return result; // Return old value



                  Readability



                  1. Use spacing in expression. Your should have a space around every binary operator in an expression (and also after branching keywords). So for ex. if(y%4) should really be if (y % 4).


                  2. You should avoid short variable names like d. By writing day instead, the code is much easier to read.


                  3. You should not write void for empty parameter lists in C++ like you did in: Date::~Date(void). Even worse, your definition is inconsistent with your declaration.


                  Validation issues



                  1. If you try to modify multiple parts of a date, like going from 2017-03-31 to 2016-02-29, it would fail if you do validation after setting the day and month but not the year.


                  2. You should also use assertion in your code to verify that you get expected results or that the code is properly used.


                  Unit testing



                  1. Before writing a library like this, you should write unit tests. Many problems like incorrect postfix operator result can easily be detected with a properly written test.

                  Conclusion



                  Given the high number of issues (between 30 and 45 issues), I would recommend you to read a bunch of good C++ books before writing more "library" code. I highly recommend Meyers and Sutter books among others.



                  Also other libraries could be used as a source of inspiration even in other language like DateTime struct in .NET. In that case, value is immutable which is an even more valuable design.






                  share|improve this answer















                  In addition to the many points raised in the 3 existing answers, there are still a lot of other issues in that design.



                  Assignment



                  1. Operator = is inefficient. If validation was properly done with exception so that it is impossible to create an invalid date, then you won't need validation… And even if you want validation, you don't need to do it 3 times.


                  2. Individual functions like setDay, setMonth and setYear does not make much sense for a date. You almost always want to set all 3 components at once. And for the very few times you would want to update only some components, you can always read parts that do no not change.


                  Construction



                  1. Constructor order is unusual. Almost everyone would assume that the order is year, month and day and doing otherwise would only cause many bugs.


                  2. Your customizable default value does not make much sense. Also, it is not thread safe. If you need some predefined values, you make some constants. Default date would usually be the minimum supported date (for ex. 0001-01-01).


                  3. Also, you should not have default values for each parameter on the constructor taking an int, a Month and another int. The default would very rarely be appropriate so if someone create a date with some defaults, then the code would be harder to understand. Also as your constructor is not explicit, it can lead to undesirable conversion constructor being called in some contexts.


                  Increment and decrement operators



                  1. Your postfix operators do not returns the old value as expected. As already mentioned by others, return type should not be const. You should almost always mimic what is done on predefined types.


                  2. Your operators does not properly handle month and year changes. If you provide such operators, then at least you should make sure that incrementing last day of a month will return first day of next month. And for decrementing, you should go to last day of previous month. And you probably also want to ensure that your date stays within the supported range.



                  3. It is recommended to code the postfix operator using the prefix operator to avoid code duplication. You can easily write something like:



                    Date operator++(int)

                    Date result(*this); // Make a copy
                    ++*this; // Increment this object
                    return result; // Return old value



                  Readability



                  1. Use spacing in expression. Your should have a space around every binary operator in an expression (and also after branching keywords). So for ex. if(y%4) should really be if (y % 4).


                  2. You should avoid short variable names like d. By writing day instead, the code is much easier to read.


                  3. You should not write void for empty parameter lists in C++ like you did in: Date::~Date(void). Even worse, your definition is inconsistent with your declaration.


                  Validation issues



                  1. If you try to modify multiple parts of a date, like going from 2017-03-31 to 2016-02-29, it would fail if you do validation after setting the day and month but not the year.


                  2. You should also use assertion in your code to verify that you get expected results or that the code is properly used.


                  Unit testing



                  1. Before writing a library like this, you should write unit tests. Many problems like incorrect postfix operator result can easily be detected with a properly written test.

                  Conclusion



                  Given the high number of issues (between 30 and 45 issues), I would recommend you to read a bunch of good C++ books before writing more "library" code. I highly recommend Meyers and Sutter books among others.



                  Also other libraries could be used as a source of inspiration even in other language like DateTime struct in .NET. In that case, value is immutable which is an even more valuable design.







                  share|improve this answer















                  share|improve this answer



                  share|improve this answer








                  edited Feb 27 at 9:28









                  Toby Speight

                  17.7k13490




                  17.7k13490











                  answered Feb 27 at 1:43









                  Phil1970

                  1913




                  1913






















                       

                      draft saved


                      draft discarded


























                       


                      draft saved


                      draft discarded














                      StackExchange.ready(
                      function ()
                      StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f188263%2fdate-class-implementation-in-c%23new-answer', 'question_page');

                      );

                      Post as a guest













































































                      Popular posts from this blog

                      Greedy Best First Search implementation in Rust

                      Function to Return a JSON Like Objects Using VBA Collections and Arrays

                      C++11 CLH Lock Implementation