1

The goal

Suppose a template <class T> class MyThing has a large number of using statements such as using T::my_def or using T::my_other_def. For obvious reasons, if T does not define my_def and my_other_def, compilation will fail. However, compilation failure in the real use-case results in hundreds of errors that make it difficult for the user to understand the real problem. While compilation failure is "good enough", I'd really like to find a way to terminate compilation early through some sort of clever proxy class. First, for completeness, the SFINAE setup is included. Second is a corollary example of how doing this with functions can be done. Then my attempts, and suspected reason for why this may not actually be possible (but am hoping to be wrong).

Note: This is a "strictly" C++11 project (but I'm not opposed to importing types, e.g. std::void_t has been successful though I failed at std::conditional). If you know these things, kip to section 2 with note:

  • The is_valid<T>() is a static constexpr bool function that behaves similar to std::conditional, checking that my_def and my_other_def are valid typenames in T.

1: SFINAE setup (C++17 backports and goodies)

///////////////////////////////////////////////////////
// backport from C++17 for using in C++11
namespace internal { template <class...> using void_t = void; }
///////////////////////////////////////////////////////
// my_def checker
/* primary template handles types that have no nested ::my_def member: */
template <class, class = internal::void_t<>>
struct has_my_def : std::false_type { };
/* specialization recognizes types that do have a nested ::my_def member: */
template <class T>
struct has_my_def<T, internal::void_t<typename T::my_def>> : std::true_type { };
///////////////////////////////////////////////////////
// my_other_def checker
/* primary template handles types that have no nested ::my_other_def member: */
template <class, class = internal::void_t<>>
struct has_my_other_def : std::false_type { };
/* specialization recognizes types that do have a nested ::my_other_def member: */
template <class T>
struct has_my_other_def<T, internal::void_t<typename T::my_other_def>> : std::true_type { };
///////////////////////////////////////////////////////
// freaking sweet: https://stackoverflow.com/a/28253503/3814202
template <bool...> struct bool_pack;
template <bool... v>
using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>;

template <class T>
static constexpr bool is_valid() {
    return all_true< has_my_def<T>::value, has_my_other_def<T>::value >::value;
}

2. The function version

This only works for calling functions. This approach (to my knowledge) cannot be used for validating the classes, because compilation will still continue. The "trick" here is just adding enough layers of indirection to function calls. I can't figure out how to do this with inheritance.

///////////////////////////////////////////////////////
// Function example
// does the real work
template <class T>
typename std::enable_if< is_valid<T>(), void>::type
do_actual_work() {
    // this never gets compiled from validate_wrapper<Almost_1>
    // and therefore never produces compiler warnings about
    // `my_other_def` not being a type
    using my_def = typename T::my_def;
    using my_other_def = typename T::my_other_def;

    my_def x = (my_def) 11;
    my_other_def y = (my_other_def) 12;

    std::cout << "A my_def: "       << x << std::endl
              << "A my_other_def: " << y << std::endl;
}

template <class T>
typename std::enable_if< !is_valid<T>(), void>::type
validate() {
    // Compilation ends here
    static_assert(is_valid<T>(), "You have been asserted.");
}

template <class T>
typename std::enable_if< is_valid<T>(), void>::type
validate() {
    std::cout << "Work gets done." << std::endl;
    do_actual_work<T>();
}

// user calls this function
template <class T>
void validate_wrapper() {
    validate<T>();
}

See section 4 for the definition of classes that demonstrate this working.

3. Attempted inheritance setup

The static_assert does go down, but the "exposed" TheClass (not internal::TheClass) will then continue on to try the using statements. For the real intent of this code, this creates hundreds of warnings which is why I'm trying to do this.

///////////////////////////////////////////////////////
// Need some more proxying?
namespace internal {
    template <class T, bool valid>
    struct TheClass {
        // compiler warning 1 (seek termination here)
        static_assert(valid, "You have been asserted.");
    };

    template <class T>
    struct TheClass<T, true> {
        std::string message() { return "You are valid."; }
    };
}

template <class T>
struct TheClass : public internal::TheClass<T, is_valid<T>()> {
    // compiler warning 2
    using my_def = typename T::my_def;
    // compiler warning 3
    using my_other_def = typename T::my_other_def;

    std::string message() {
        return internal::TheClass<T, is_valid<T>()>::message();
    }
};

4. some simple tests

///////////////////////////////////////////////////////
// The tests
struct Good {
    using my_def = float;
    using my_other_def = int;
};
struct Almost_1 { using my_def = double; };
struct Almost_2 { using my_other_def = bool; };
struct Bad {};

int main(int argc, const char **argv) {
    // sanity checks
    std::cout << std::boolalpha
              << "Good:     " << is_valid<Good>()     << std::endl
              << "Almost_1: " << is_valid<Almost_1>() << std::endl
              << "Almost_2: " << is_valid<Almost_2>() << std::endl
              << "Bad:      " << is_valid<Bad>()      << std::endl;

    // in function land, uncomment these and it will
    // only produce the single static_assert for valid<T>
    // rather than continuing on for breaking T
    // validate_wrapper<Good>();
    // validate_wrapper<Almost_1>();

    // At least this works.
    TheClass<Good> g;
    std::cout << "Good message: " << g.message() << std::endl;

    // I would like to achieve just one error from static_assert
    // comment out to see that `validate_wrapper<Almost_1>` will
    // terminate early (as desired)
    TheClass<Almost_1> a1;
    return 0;
}

So in the end, if you leave the function version uncommented in main, you can observe the same behavior that I'm trying to avoid.

  1. The static_assert for validate_wrapper<Almost_1> is triggered.
  2. Since we are in the same code block, TheClass<Almost_1> is still going to get compiled and produce more warnings.
    • If you comment out TheClass<Almost_1>, you won't get warnings from do_actual_work.
    • The analogy for the class version is that main is currently being compiled, so it's going to see it through to the end (kind of the whole point of SFINAE as I understand it). So since TheClass<Almost_1> has already started to compile, it goes until the end regardless of the static_assert going on in its base class.

Is there any clever way of deferring inheritance? I tried a variant of the curiously recurring template pattern thinking that would be how to do this, but it actually ended up just producing more warnings.

svenevs
  • 833
  • 9
  • 24
  • 3
    I might be in stupid mode, but I can't find where you have stated what you actually want to do... – Quentin Sep 22 '17 at 08:51
  • No, it is I! I tried like heck to make the question as short as possible, and in doing so effectively removed the actual question. Question leadup and synopsis at end should make things clearer now, by comparing it to how the function version works. – svenevs Sep 22 '17 at 22:02

1 Answers1

0

Hopefully there is another way, but it seems the easiest solution is to go the other way: the class that users are supposed to instantiate has no actual implementation, and the internal class does it all.

namespace internal {
    template <class T, bool valid>
    struct TheClass {
        static_assert(valid, "Template 'class T' must have static types `my_def` and `my_other_def`.");
    };

    template <class T>
    struct TheClass<T, true> {
        // compiler warning 2
        using my_def = typename T::my_def;
        // compiler warning 3
        using my_other_def = typename T::my_other_def;

        std::string message() {
            return "work gets done";
        }
    };
}

template <class T>
struct TheClass : internal::TheClass<T, is_valid<T>()> { };

struct Good {
    using my_def = float;
    using my_other_def = int;
};
struct Almost_1 { using my_def = double; };

int main(int argc, const char **argv) {
    TheClass<Good> g;
    std::cout << "Good message: " << g.message() << std::endl;

    // only one static assert
    TheClass<Almost_1> a1;
    return 0;
}
svenevs
  • 833
  • 9
  • 24