5
\$\begingroup\$

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:

  1. void print_time(int mins, double secs)
  2. void print_time(double total_secs)
  3. 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

Live Demo

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.

\$\endgroup\$
2
  • \$\begingroup\$ Having both 2 and 3 as possibly existing members is an abomination, as implicit conversion of the arguments does ignore the different scales. \$\endgroup\$ – Deduplicator Aug 24 '17 at 10:59
  • \$\begingroup\$ @Deduplicator "Abomination" seems a bit harsh for a contrived example but I agree with the sentiment. I'll make sure to avoid any such faults with the actual callback API. \$\endgroup\$ – Logan Barnes Aug 24 '17 at 13:23
5
\$\begingroup\$

Not a full review, just an answer on the SFINAE-mechanisms part:

I'd say that your method is perfectly idiomatic. I recommend an approach exactly isomorphic to yours, based on some code I originally saw being used by Eric Niebler:

template<size_t I> struct priority_tag : priority_tag<I-1> {};
template<> struct priority_tag<0> {};

Notice that by using size_t we prevent anyone from entering an infinite regress via priority_tag<-1>; although a ((1<<64)-1)-level regress is just as bad, honestly, and compilers will catch both in practice after a few levels, so whatever.

I also recommend passing priority_tag<I>/Precedence<I> by value instead of by const-reference, since it's an empty type. Again since it'll all get inlined I don't think it really matters either way.


As for other ways to do the ladder of possibilities: In this case, because you're doing expression SFINAE, I don't think you can get any better. If you were willing to name your constraints, via

inline constexpr bool is_min_sec_printable_v<T> = ...;
inline constexpr bool is_sec_printable_v<T> = ...;
inline constexpr bool is_min_printable_v<T> = ...;

then you could use C++17 if constexpr to write:

template<typename T>
auto print(const T& printer) {
    if constexpr (is_min_sec_printable_v<T>) {
        printer.print_time(mins_, secs_);
    } else if constexpr (is_sec_printable_v<T>) {
        printer.print_time(total_secs_);
    } else if constexpr (is_min_printable_v<T>) {
        printer.print_time(mins_);
    } else {
        std::cout << "nothing\n";
    }
}

Notice that << std::endl is just an alias for << '\n' << std::flush, and I don't think your print_time function ought to be concerned with flushing the stream, so either print ought to flush unconditionally after the ladder, or else you ought to push the responsibility for flushing up to your caller (the approach I took).


In C++2a, you might one day be able to use

if constexpr(requires( printer.print_time(mins_, secs_) )) {
    printer.print_time(mins_, secs_);
} else if constexpr ...

to eliminate the named concepts is_min_sec_printable_v etc. I'm ambivalent as to whether this would be an improvement. (It continues to suffer from the "Write Everything Twice" problem that infects most of modern C++; cf. the decltype repetition in your original question.)

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I believe it would be great to mention std::chrono::seconds or milliseconds, but I'm not sure if the context in the question is at least somewhat related to the problem at hand. \$\endgroup\$ – Incomputable Aug 20 '17 at 2:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.