Domain objects
In code bases I regularly stumble upon use of primitive types with a particular interpretation. Examples are:
- int as seconds
- int x, int y as point location
- int w, int h as size
- string for file- and directory names
Using primitive types has some drawbacks:
- no implicit documentation value
- error prone with any integer fits the argument
- possibility to swap arguments, e.g. in case width and height
In this situation it's better to use domain objects which are compile time safe and have an intrinsic doucmentation value. For example in above case it it is preferred to use:
- std::chrono::seconds
- Point
- Size
- std::filesystem::path
A complete example for time becomes then as follow:
#include <chrono>
using namespace std::chrono_literals;
void SetDurationAlarm(int seconds);
void SetDuration(std::chrono::seconds s);
// client:
SetDuration(100); // 100 what?
SetDuration(100s); // 100 seconds no doubt
Example primitve type arguments:
// correct but error prone
void SetRect(int l, int t, int r, int b);
// client
constexpr int x = 0;
constexpr int y = 0;
constexpr int w = 100;
constexpr int h = 200;
SetRect(x, y, w, h); // compiles but wrong
SetRect(x, y, x + w, y + h); // ok
It's better to use domain objects:
struct Point
{
int m_x;
int m_y;
};
struct Size
{
int m_cx;
int m_cy;
};
void SetRect(const Point& rpt, const Size& rsz);
constexpr Point pt{0, 0};
constexpr Size size{100, 200};
SetRect(pt, size);
A careful reader may argue that on the definiton of point and size primitive types are used. Indeed there is an object creation location where you have to take care but the rest of the argument passing is compile time safe when using domain objects as 'Point' and 'Size'.
Example of argument passing of primitive types:
// correct but error prone
struct Image
{
Image(int x, int y, int w, int h); //watch out
};
// client
void FooImage(int x, int y, int w, int h)
{
Image img{x, y, w, h};
SetRect(x, y, x + w, y + h);
SetLocation(x, y);
SetSize(w, h);
}
Example of using domain objects in argument passing:
struct Image
{
Image(const Point& rpt, const Size& rsz);
};
void FooImage(const Point& rpt, const Size& rsz)
{
Image img{rpt, rsz};
SetRect(rpt, rsz);
SetLocation(rpt);
SetSize(rsz);
}
C++ standard
Unfortuantely C++ does not define many frequently used primtive types like points, size and rects. <chrono> and <filesystem> were only added in C++11; before that there was no duration type and one had to use strings for files and directories.
Not sure of things become better in the future since the committee is more interested in adding yet another obscure language feature than adding much needed library support for standard use (e.g. networking; graphics; persistence; cryptography).
So we ended up that many libraries define their own point, size and rect. This ofc unfortunate with possible conversions errors and that one has to learn a dozen of point classes; each with their own subtle semantics:
Windows |
POINT |
MFC |
CPoint |
Direct2D |
D2D1_POINT_2U |
OpenCV |
cv::Point2i |
Boost.Geometry |
boost::geometry::model::d2::point_xy |
glm |
vec4 |
Note: Boost.Geometry is actual flexible and in 3D vectors are used for points as well.
External links
- https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rp-direct