jameszhao00
jameszhao00

Reputation: 7301

C++ Compile-time Duck Typing with Interfaces

Is there a way to do something like the following in C++

template<typename TAnimal>
bool can_eat(TAnimal& animal) where bool TAnimal::IsAlive() exists
{
    return !animal.IsAlive();
}

//...

Duck duck;
assert(can_eat(duck) == true); //compiles

Wood wood;
can_eat(wood); // fails to compile because wood doesn't have IsAlive()

The explicit interface, in my opinion, makes it more clear what the function expects. However, I don't want to create an actual interface class (very tedious).

Upvotes: 2

Views: 1837

Answers (6)

Luc Danton
Luc Danton

Reputation: 35439

Do not use enable_if to enforce your requirements. Using enable_if will make the function 'disappear', which can be quite confusing for the user. Typical symptom is an error message such as error: no matching function for call to expression. That doesn't exactly convey to the user that a requirement violated.

You should instead enforce your requirements using static_assert, assuming C++0x. If you're using C++03, whether you should be using an emulation of static_assert (e.g. Boost's STATIC_ASSERT) or not is a toss-up since that usually means trading one error message for the other.

Contrast:

// SFINAE for types that do not decay to int
template<
    typename T
    , typename = typename std::enable_if<
        std::is_same<
            typename std::decay<T>::type
            , int
        >::value
    >::type
>
void
f(T&&)
{}

// using static assert instead
template<
    typename T
>
void
g(T&&)
{
    static_assert( std::is_same<typename std::decay<T>::type, int>::value
                 , "Constraints violation" );
}

Using GCC I get the following error for doing f("violation") (both messages come with filename and line number):

error: no matching function for call to 'f(const char [10])'

On the other hand, g("violation") yields:

error: static assertion failed: "Constraints violation"

Now imagine that you use clear, explicit messages in your assertions such as foo: parameter type must be CopyConstructible inside template foo.

With that said, SFINAE and static_assert are somewhat antagonistic, thus having both explicit constraint violation messages and clever overloads isn't always possible and/or easy.


What you want to do is easily achieved using Boost.ConceptCheck. It does however require to write out-of-line code: the constraints class. I also don't think it uses static_assert where available, so the error messages might not be as nice. This could be changed in the future.

Another possibility is to use static_assert + type traits. What's interesting with that approach is that with C++0x the library comes with a bevy of useful traits, which you can use right out of the box without writing out-of-line code. Even more interesting is that the use of traits is not limited to writing constraints, they can also be used with SFINAE to make clever overloads.

However, there is no trait that is available off-hand to check whether a type supports a particular member of operation, possibly due to the way C++ handles the names of functions. We can't use something like has_member<T, &T::member_to_test_for> either because that would only make sense if the member we were testing for existed in the first place (disregarding things like overloads and the fact that we also need to pass the signature of the member to the trait).

Here's how to transform an arbitrary expression into a trait:

template<typename T>
struct void_ {
    typedef void type;
};

template<typename T>
struct trait {
private:
    typedef char yes[1];
    typedef char no[2];

    template<typename U>
    static
    yes&
    test(U&&
        , typename void_<decltype( std::declval<U&>().member() )>::type* = 0);

    static
    no&
    test(...);

public:
    static constexpr bool value = sizeof test(std::declval<T>()) == sizeof(yes);
};

Notice how sizable this is. Writing a Boost.ConceptCheck constraints class might be easier (but remember, not reusable for SFINAE).

The arbitrary expression is std::declval<U&>().member(). Here, the requirements are that given an lvalue reference of U (or T for the case where the trait is true, if you will), then calling member() on it is valid.

You could also check that the type of that expression (i.e. the result type of whatever overload of member has been picked for this expression) is convertible to a type (do not check whether it is that type; that's too restrictive for no good reason). That would inflate the trait however, again making this in favour of a constraints class.

I do not know of a way to make a static_assert part of the signature of a function template (this seems to be something you want), but it can appear inside a class template. Boost.ConceptCheck doesn't support that either.

Upvotes: 9

Konrad Rudolph
Konrad Rudolph

Reputation: 545488

Boost offers BOOST_STATIC_ASSERT for this. The just recently approved version of the C++ standard will offer a built-in version of a static_assert macro.

enable_if, on the other hand, isn’t really well suited for this. It can be used but the primary purpose of enable_if is to distinguish between otherwise ambiguous overloads.

Upvotes: 2

jcoder
jcoder

Reputation: 30035

As others have said this will just work. it won't be able to instantiate the template if the function does not exist.

The boost library contains some classes to assist with this kind of thing though, for example enable_if which can be used to only "enable" a template where a condition is true. There is also the type traits library which is kind of related, you might be able to use this to determine at compile time if the function you want to call exists.

I have to admit I've not used any of this myself, but it looks to me like you should be able to use it to achieve what you want...

Upvotes: 0

Ken Wayne VanderLinde
Ken Wayne VanderLinde

Reputation: 19339

This is something that concepts were intended to solve.

Concepts were a proposed addition to the latest C++ standard, but were dropped because the commitee wasn't convinced that they were solid enough to include in the language. See what Herb Sutter wrote about their exclusion from the standard.

Technically, concepts are unneeded, as template simply use whatever they can (i.e. lose the where clause, and you have what you're asking for). If the required method isn't there at compile time, then the code simply will not compile. But concepts would give the coder more explicit control over the type's interface, and would give much more reasonable error message than currently provided by most compilers.

Upvotes: 5

Benjamin Lindley
Benjamin Lindley

Reputation: 103693

where void TAnimal::IsAlive() exists

I assume you mean bool TAnimal::IsAlive()? If so, C++ already does what you are asking. If Duck has the IsAlive() method, then this will compile:

Duck duck;
assert(can_eat(duck) == true); //compiles

If Wood does not have the IsAlive() method, this will not compile:

Wood wood;
can_eat(wood); // fails to compile because wood doesn't have IsAlive()

That's what you're asking for right?

Upvotes: 1

John Zwinck
John Zwinck

Reputation: 249093

You don't have to do anything--just omit the hypothetical "where ... exists" from your example and it is normal C++ code that works.

If you insist on having the function be available only under some condition, you might try combining boost::enable_if with has_member from here: http://lists.boost.org/Archives/boost/2002/03/27229.php

The idea being that you would only allow the template function to be instantiated if some condition was met...but since SFINAE the compiler is basically going to do that for you already in the case where the condition is the same as the actual compile-time needs of the function (as in your example).

Upvotes: 0

Related Questions