Instead of a template parameter, have a single parameter that encodes the specific kind of action in its type.
class Color {
private:
uint8_t r_;
uint8_t g_;
uint8_t b_;
public:
Color(uint8_t r, uint8_t g, uint8_t b):
r_{r}, g_{g}, b_{b} {};
};
class Opacity {
private:
uint8_t alpha_;
public:
Opacity(uint_8 alpha): alpha_{alpha} {};
};
class Rectangle {
private:
uint32_t x_;
uint32_t y_;
uint32_t w_;
uint32_t h_;
public:
Rectangle(uint32_t x, uint32_t y, uint32_t w, uint32_t h):
x_{x}, y_{y}, w_{w}, h_{h} {};
};
// etc.
And then use a function overload instead of a template.
class Context {
public:
Set(const Color& c);
Set(const Opacity& o);
Draw(const Rectangle& rect);
// etc.
}
The usage is at least very close to what you try to achieve.
Set(Color{255, 255, 255});
Set(Opacity{128});
Draw(Rectangle{0, 0, 50, 50});
Set(Opacity{255});
Draw(Texture{Textures::Woman, 75, 75});
You also might like to have different Rectlangle constructors, one with two points and one with a point and width and height. There you can use the same strategy:
class Width {
private:
uint32_t w_
public:
Width(uint32_t w): w_{w} {};
operator uint32_t () { return w_; };
};
class Height {
// same as Width
};
class Rectangle {
private:
uint32_t x1_;
uint32_t y1_;
uint32_t x2_;
uint32_t y2_;
public:
Rectangle(uint32_t x, uint32_t y, Width w, Height h):
x1_{x}, y1_{y}, x2_{x+w}, y2_{y+h} {};
Rectangle(uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2):
x1_{x1}, y1_{y1}, x2_{x2}, y2_{y2} {};
};
// usage
Draw(Rectangle{0, 0, Width{50}, Height{50}}); // use width and height
Draw(Rectangle{0, 0, 50, 50}); // use two points
You can even go further and define a Point type, so that drawing a rectangle will look like this:
Draw(Rectangle{Point{0, 0}, Width{50}, Height{50}}); // use width and height
Draw(Rectangle{Point{0, 0}, Point{50, 50}}); // use two points
Use techniques like that to find the perfect balance between stating of intent and reducing noise.
And of course you can add simple template functions to your Context:
template<typename T, typename A...>
Set(A&& args...) {
Set(T{std::forward(args)...});
}
template<typename T, typename A...>
Draw(A&& args...) {
Draw(T{std::forward(args)...});
}
Now you can use the whole machine as you originally intended.
Set<Color>(255, 255, 255);
Set<Opacity>(128);
Draw<Rectangle>(0, 0, 50, 50);
Set<Opacity>(255);
Draw<Texture>(Textures::Woman, 75, 75);
// With width and height types
Draw<Rectangle>(0, 0, Width{50}, Height{50});
Draw<Rectangle>(0, 0, 50, 50);
// With Point Type
Draw<Rectangle>(Point{0, 0}, Width{50}, Height{50});
Draw<Rectangle>(Point{0, 0}, Point{50, 50});