In the process of over-engineering a custom library, I implemented expression SFINAE to detect and call specific member functions only if they exist. I also let classes overload these functions and then I call the "best" available function based on an order of precedence.
Example
The library will accept classes that have defined specific member functions based on a list of available templates:
void print_time(int mins, double secs)
void print_time(double total_secs)
void print_time(int mins)
If a class defines one of the functions listed then the library will call the appropriate function. If a class overloads multiple functions, the first one on the list will be called:
class MadLadPrinter {
public:
void print_time(int mins, double secs); // <- this will get called
void print_time(int mins);
};
...
MadLadPrinter mlp;
PrinterLibrary::print_time(mlp); // calls 'mlp.print_time(int, double)'
The function selection makes more sense in the context of the actual library which contains a large set of callbacks with varying data that classes may or may not need. The code below is a contrived example to illustrate the basic functionality even if the usage in this case is overkill (although it's arguably overkill in the library too).
Sfinay Printers:
#include <iostream>
// custom struct to ensure functions are called in the correct order
template<int N>
struct Precedence : public Precedence<N - 1> {};
template<>
struct Precedence<0> {};
// The main printer that delegates function calls of other classes.
// Its 'print' function accepts classes that have defined:
// - void print_time(int mins, double secs)
// - void print_time(double total_secs)
// - void print_time(int mins)
// or none of the above.
class PrinterDelegator {
public:
// The Precedence variable ensures the function with the highest 'N' value
// gets called. If the function with the current 'N' value is ill-formed,
// the Precedence struct will cast to its next highest base class until
// a properly formed function is available.
template<typename T>
void print(const T &printer) {
print(printer, p_);
}
private:
// these would all actually be incrementing
int mins_{3};
double secs_{22.5};
double total_secs_{202.5};
Precedence<3> p_{};
// The "best" function. Call this if available
template<typename T>
auto print(const T &printer, const Precedence<3>&) -> decltype(printer.print_time(mins_, secs_)) {
printer.print_time(mins_, secs_);
}
// Other available functions ordered by importance
template<typename T>
auto print(const T &printer, const Precedence<2>&) -> decltype(printer.print_time(total_secs_)) {
printer.print_time(total_secs_);
}
template<typename T>
auto print(const T &printer, const Precedence<1>&) -> decltype(printer.print_time(mins_)) {
printer.print_time(mins_);
}
// default empty definition allowing for classes that haven't defined 'print_time'
template<typename T>
auto print(const T &printer, const Precedence<0>&) -> decltype(void()) {
std::cout << "nothing" << std::endl;
}
};
// class with all possible functions. Only the "best"
// one will get called: 'void print_time(int, double)'
class VerbosePrinter {
public:
void print_time(int mins, double secs) const {
print_time(mins);
print_time(secs);
}
void print_time(double total_secs) const {
std::cout << total_secs << "s" << std::endl;
}
void print_time(int mins) const {
std::cout << mins << "m" << std::endl;
}
};
// Class with two of the function options. The best one here is
// 'void print_time(double)' so that one will be called
class SinglePrinter {
public:
void print_time(double total_secs) const {
std::cout << total_secs << "s" << std::endl;
}
void print_time(int mins) const {
std::cout << mins << "m" << std::endl;
}
};
// Empty class that can still be used even though no functions are defined
class EmptyPrinter {};
int main() {
PrinterDelegator pd;
VerbosePrinter vp;
std::cout << "Should print minutes and seconds:" << std::endl;
pd.print(vp);
SinglePrinter sp;
std::cout << "Should print total seconds:" << std::endl;
pd.print(sp);
EmptyPrinter ep;
std::cout << "Should print nothing:" << std::endl;
pd.print(ep);
return 0;
}
Output:
Should print minutes and seconds: 3m 22.5s Should print total seconds: 202.5s Should print nothing: nothing
Now assuming this functionality is actually needed, is there a better way (compile-time) to handle the function selection instead of with the Precedence
struct? The recursive base class hierarchy seems a bit hacky.
Also, is there a more elegant way to handle the detection and delegation of available member functions in general? Perhaps using C++14/17 syntax?
In order to keep classes lightweight and efficient, I'm intentionally avoiding any virtual inheritance options for the printer classes.