the goal is several scoped enums (each in it own struct/class/namespace) with some associated utility functions. I've used that pattern many times and the utility functions always include a toString() and fromString() function for human intelligible i/o.
However, this time I've got several different types of dimensions: Length, Time, Angle, ... that I want to handle Unit's for so the naming pattern of these structs structs/classes/namespaces is Unit. And these different classes are practically identical except each one has an enum of units specific to that dimension.
for the LengthUnit struct/class/namespace the enum is currently
enum type
{
meters=0,
kilometers,
SIZE
};
obviously I could add centimeters, millimeters, micrometers, miles, feet, yards etc, but I don't need those right now
for the TimeUnit struct/class/namespace the enum is
enum type
{
seconds=0,
milliseconds,
microseconds,
nanoseconds,
SIZE
};
for the AngleUnit struct/class/namespace the enum is
enum type
{
degrees=0,
radians,
SIZE
};
each Unit class has 4 identically (across dimensions) named functions
static std::string toString(type unit);
static type fromString(std::string unitString);
static std::string toAbbreviation(type unit);
static type fromAbbreviation(std::string unitString);
the toString() and toAbbreviation() functions are specific to the Type because each contains a custom switch statement to convert from unit to string.
the fromString() and fromAbbreviation() function have a common implementation, they loop over the enum lists essentially a for(type unit=0; unit<SIZE; ++unit) (with some static_cast-ing) to compare the string to the one coming out of toString() or toAbbreviation() when it finds a match it breaks, if it doesn't find a match it returns SIZE, errors are thrown exceptions
and each unit has function whose name is specific to the type of dimension, for example static double lengthInMeters(const type unit): //for unit=LengthUnit::meters this returns 1.0, for unit=LengthUnit::kilometers it returns 1000.0 static double timeInSeconds(const type unit); //for unit=TimeUnit::seconds this returns 1.0, for unit=TimeUnit::milliseconds it returns 1e-3, etc. static double angleInDegrees(const type unit);
to decrease boiler plate code (because the fromString() and fromAbbreviation() function have identical implementations across dimension types) I tried to template the common class and use the CRTP to do it, the relevant code you need to compile and reproduce the same error is
#include <algorithm>
#include <cctype>
#include <sstream>
#include <exception>
#include <cstring>
class StringException : public std::exception
{
protected:
const std::string m_message;
public:
StringException(const std::string& message) : m_message(message) {};
virtual const char* what() const throw()
{
return m_message.c_str();
};
};
inline std::string toLower(std::string str)
{
std::transform(str.begin(), str.end(), str.begin(),
[](unsigned char c){ return std::tolower(c); });
return str;
};
struct DimensionType
{
enum type
{
Length=0,
Time,
Angle,
SIZE
};
std::string toString(const type dimType=SIZE)
{
std::ostringstream oss;
switch(dimType)
{
case Length: return std::string("Length");
case Time: return std::string("Time");
case Angle: return std::string("Angle");
case SIZE:
oss << "SIZE=" << SIZE;
return oss.str();
default:
oss << "DimensionType::toString():\n "
<< "We either encountered an invalid DimensionType value OR\n "
<< "DimensionType::toString() needs to be updated to handle a new DimensionType value\n";
throw(StringException(oss.str()));
}
};
type fromString(const std::string dimString)
{
for(type dimType=Length; dimType<SIZE; dimType=static_cast<type>(dimType+1))
{
try
{
if(dimString.compare(toString(dimType))==0)
return dimType;
}
catch(std::exception& e)
{
std::ostringstream oss;
oss << "DimensionType::fromString():\n "
<< e.what();
throw(StringException(oss.str()));
}
}
return SIZE;
};
};
typedef DimensionType::type DimensionType_t;
template <class T>
class DimensionUnit
{
public:
static const std::string DimName;
static const DimensionType_t DimType;
static typename T::type fromString(std::string unitString)
{
unitString=toLower(unitString);
for(typename T::type unit=static_cast<typename T::type>(0); unit<T::SIZE; unit=static_cast<typename T::type>(unit+1))
{
try
{
if(unitString.compare(typename T::toString(unit))==0)
return unit;
}
catch(std::exception& e)
{
std::ostringstream oss;
oss << DimName << "Unit::fromString():\n "
<< e.what();
throw(StringException(oss.str()));
}
}
return T::SIZE;
};
static typename T::type fromAbbreviation(std::string unitString)
{
unitString=toLower(unitString);
for(typename T::type unit=static_cast<typename T::type>(0); unit<T::SIZE; unit=static_cast<typename T::type>(unit+1))
{
try
{
if(unitString.compare(typename T::toAbbreviation(unit))==0)
return unit;
}
catch(std::exception& e)
{
std::ostringstream oss;
oss << DimName << "Unit::fromAbbreviation():\n "
<< e.what();
throw(StringException(oss.str()));
}
}
return T::SIZE;
};
};
class LengthUnit : public DimensionUnit<LengthUnit>
{
public:
enum type
{
meters=0,
kilometers,
SIZE
};
static double lengthInMeters(const type unit)
{
std::ostringstream oss;
switch(units)
{
case meters: return 1.0;
case kilometers: return 1000.0;
case SIZE:
oss << "LengthUnit::lengthInMeters():\n "
<< "SIZE is not a LengthUnit value. It is a count of defined LengthUnit values\n "
<< "that enables iteration/looping.\n";
throw(StringException(oss.str()));
default:
oss << "LengthUnit::lengthInMeters():\n "
<< "We either encountered an invalid LengthUnit value OR\n "
<< "LengthUnit::lengthInMeters() needs to be updated to handle a new LengthUnit value\n";
throw(StringException(oss.str()));
}
};
static std::string toString(const type unit=SIZE)
{
std::ostringstream oss;
switch(units)
{
case meters: return std::string("meters");
case kilometers: return std::string("kilometers");
case SIZE:
oss << "SIZE=" << SIZE;
return oss.str();
default:
oss << "LengthUnit::toString():\n "
<< "We either encountered an invalid LengthUnit value OR\n "
<< "LengthUnit::toString() needs to be updated to handle a new LengthUnit value\n";
throw(StringException(oss.str()));
}
};
static std::string toAbbreviation(const type unit=SIZE)
{
std::ostringstream oss;
switch(units)
{
case meters: return std::string("m");
case kilometers: return std::string("km");
case SIZE:
oss << "SIZE=" << SIZE;
return oss.str();
default:
oss << "LengthUnit::toAbbreviation():\n "
<< "We either encountered an invalid LengthUnit value OR\n "
<< "LengthUnit::toAbbreviation() needs to be updated to handle a new LengthUnit value\n";
throw(StringException(oss.str()));
}
};
};
typedef LengthUnit::type LengthUnit_t;
const std::string LengthUnit::DimName("Length");
const DimensionType_t LengthUnit::DimType(DimensionType::Length);
inline std::ostream& operator<<(std::ostream& os, LengthUnit_t unit)
{
os << "( " << LengthUnit::toAbbreviation(unit) << ")";
return os;
};
this is my first time attempting to use an CRTP because I've never had that kind of boilerplate similarity before and the errors I'm getting are of the form
error: invalid use of incomplete type ‘class LengthUnit’
static typename T::type fromString(std::string unitString)
^~~~~~~~~~
note: forward declaration of ‘class LengthUnit’
class LengthUnit : public DimensionUnit<LengthUnit>
^~~~~~~~~~
error: invalid use of incomplete type ‘class LengthUnit’
static typename T::type fromAbbreviation(std::string unitString)
^~~~~~~~~~~~~~~~
note: forward declaration of ‘class LengthUnit’
class LengthUnit : public DimensionUnit<LengthUnit>
google seems to indicate I've got an infinite recursion problem, but I'm hoping there's a smallish change I could make to avoid it.