3

I am trying to provide an interface description for a free function listenTo(SomeAnimal) that should operate on types that fulfil particular type requirements (it should be an animal). The function arguments should not use the mechanism of interface inheritance with pure virtual methods.

I hacked a solution where the free function checks the argument type via an sfinae statement for a base class. To guarantee that the argument implements the interface of the base class I deleted the base class methods using = delete. I did not find any similar solution on the internet, thus, I am not sure if it makes sense, but it works.

Here it is, any opinions ?

#include <iostream>
#include <type_traits>

class IAnimal {
public:
    // Interface that needs to be implemented
    std::string sound() const = delete;
protected:
    IAnimal(){}
};


class Cat : public IAnimal {
public:
    // Implements deleted method
    std::string sound() const {
        return std::string("Meow");
    }

};

class WildCat : public Cat {
public:
    // Overwrites Cat sound method
    std::string sound() const {
        return std::string("Rarr");
    }

};

class Dog : public IAnimal{
public:
    // Implements deleted method
    std::string sound() const {
        return std::string("Wuff");
    }
};


class Car {
public:
    // Implements deleted method
    std::string sound() const {
        return std::string("Brum");
    }
};



// Sfinae tests for proper inheritance
template<class TAnimal,
         typename = std::enable_if_t<std::is_base_of<IAnimal, TAnimal>::value> >
void listenTo(TAnimal const & a ) {
    std::cout << a.sound() << std::endl;
}


int main(){

    // Objects of type IAnimal can not be instanciated
    // IAnimal a;

    // Cats and Dogs behave like IAnimals
    Cat cat;
    WildCat wildCat;
    Dog dog;
    Car car;

    listenTo(cat);
    listenTo(wildCat);
    listenTo(dog);

    // A car is no animal -> compile time error
    // listenTo(car);

    return 0;
}
erikzenker
  • 752
  • 5
  • 18
  • 2
    You can always omit the definition (instead of `= delete;`) – Rakete1111 Sep 21 '16 at 16:40
  • Yes you can, but then you get a not so nice `undefined reference to` error from the compiler (in the case you ignore the interface). – erikzenker Sep 21 '16 at 16:42
  • 1
    I think in Stroustup's book (The C++ Programming Language 2013) he describes about that approach, with a similar example. It looks good to me, but I am hardly qualified to assess it. – Aganju Sep 21 '16 at 16:53
  • Out of interest, why use inheritance at all, if `sound()` is not to be polymorphic? – Richard Hodges Sep 21 '16 at 17:00
  • 1
    Might you be looking for `virtual std::string sound() const = 0` ? To make the function "pure virtual" and thus force derived classes to provide an implementation? – Jesper Juhl Sep 21 '16 at 17:03
  • You might want to ask a more specific question than "any opinions ?"; see [here](https://stackoverflow.com/help/dont-ask) – anatolyg Sep 21 '16 at 17:04
  • As I have mentioned, I am not looking for a pure virtual method solution. I want to use inheritance to provide the client of the function an interface definition of the template type. – erikzenker Sep 21 '16 at 21:57
  • You could just omit `sound` entirely from the base class, since it is not used anywhere – M.M Sep 21 '16 at 23:04

5 Answers5

2

C++ doesn't have yet Concepts :-( but gcc-6 implements it:

template <class T>
concept bool Animal() { 
    return requires(const T& a) {
        {a.sound()} -> std::string;
    };
}

void listenTo(const Animal& animal) {
    std::cout << animal.sound() << std::endl;
}

Demo

But you can create traits relatively easily with is-detected:

typename <typename T>
using sound_type = decltype(std::declval<const T&>().sound());

template <typename T>
using has_sound = is_detected<sound_type, T>;

template <typename T>
using is_animal = has_sound<T>;
// or std::conditional_t<has_sound<T>::value /*&& other_conditions*/,
//                       std::true_type, std::false_type>;

And then regular SFINAE:

template<class T>
std::enable_if_t<is_animal<T>::value>
listenTo(const T& animal) {
    std::cout << animal.sound() << std::endl;
}
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Jarod42
  • 203,559
  • 14
  • 181
  • 302
1

Another way, avoiding the complication of inheritance, is to create a type trait:

#include <iostream>
#include <type_traits>

template<class T>
struct is_animal : std::false_type {};

class Cat {
public:
    std::string sound() const {
        return std::string("Meow");
    }
};
template<> struct is_animal<Cat> : std::true_type {};

class WildCat : public Cat {
public:
    // Overwrites Cat sound method
    std::string sound() const {
        return std::string("Rarr");
    }

};
template<> struct is_animal<WildCat> : std::true_type {};

class Dog {
public:
    std::string sound() const {
        return std::string("Wuff");
    }
};
template<> struct is_animal<Dog> : std::true_type {};


class Car {
public:
    std::string sound() const {
        return std::string("Brum");
    }
};



// Sfinae tests for proper inheritance
template<class TAnimal,
typename = std::enable_if_t<is_animal<TAnimal>::value> >
void listenTo(TAnimal const & a ) {
    std::cout << a.sound() << std::endl;
}


int main(){

    // Objects of type IAnimal can not be instanciated
    // IAnimal a;

    // Cats and Dogs behave like IAnimals
    Cat cat;
    WildCat wildCat;
    Dog dog;
    Car car;

    listenTo(cat);
    listenTo(wildCat);
    listenTo(dog);

    // A car is no animal -> compile time error
    // listenTo(car);

    return 0;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • Thank you for this nice solution. Is there a way to describe the interface of the type TAnimal ? – erikzenker Sep 21 '16 at 22:16
  • 1
    @erikzenker if you mean "is there a name for this kind of traits-based SFNAE", I'm not sure what the technical name would be. If you mean "what is TAnimal"? Then in this case I'd say it was the "concept of an animal", and the trait is indicating that the class represents a model of that concept. The traits and policies we create around the TAnimal describe its conceptual behaviour, abilities and properties. – Richard Hodges Sep 21 '16 at 22:33
1
namespace details {
  template<template<class...>class Z, class always_void, class...Ts>
  struct can_apply:std::false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z,void,Ts...>;

This is a meta type trait that helps write other type traits.

template<class T>
using sound_result = decltype( std::declval<T>().sound() );

sound_result<T> is the result of t.sound() where t is of type T.

template<class T>
using can_sound = can_apply<sound_result, T>;

can_sound<T> is a true type if and only if t.sound() is valid to call.

We can now say that animals are things that can sound.

template<bool b>
using bool_t = std::integral_constant<bool, b>;

template<class T>
using is_animal = bool_t< can_sound<T>{} >; // add more requirements

template<class TAnimal,
  std::enable_if_t< is_animal<TAnimal const&>{}, int> =0
>
void listenTo(TAnimal const & a ) {
  std::cout << a.sound() << std::endl;
}

We get an error saying there is no matching overload if we try to listenTo(0) or somesuch.

Requiring that .sound() return something streamable can be written as well.

template<class T>
using stream_result = decltype( std::declval<std::ostream&>() << std::declval<T>() );

template<class T>
using can_stream = can_apply< stream_result, T >;

template<class T>
using stream_sound_result = stream_result< sound_result< T > >;

template<class T>
using can_stream_sound = can_apply< stream_sound_result, T >;

Now we can upgrade our animal test:

template<class T>
using is_animal = bool_t< can_stream_sound<T>{} >;
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Wow a really complex solution, but I kind of like it. It is a lot of boilerplate code. Thus, is there a way to shorten it maybe by a library or macros you know ? – erikzenker Sep 21 '16 at 22:14
  • This tests for the member function sound(), but the car has this too, and you can't listenTo() a car (defined in the OP's question). You still need a trait or tag to indicate that it's listenTo()-able. – Richard Hodges Sep 21 '16 at 22:37
  • 1
    @erikzen half of it is a library, namely the `can_apply`. It is a dozen lines. C++20 may have an equivalent tenplate called `is_detected`. To write `can_x` you need one `using X=decltype` template line. It can be as complex as you like, or as simple. That is you defining the "contract" of the concept, like your `=delete` base class does. So 2 lines per feature in the concept, and a line or so to glue them into one named concept (`is_animal` for example). You could use some macros to drop it to one line per feature, but I wouldn't. – Yakk - Adam Nevraumont Sep 22 '16 at 00:07
1

You didn't ask for an alternative solution. Instead, you asked for an opinion about your solution.
Well, here is my opinion, hoping it can help you.


That's a weak sfinae expression. You can easily break it using:

listenTo<Car, void>(car);

At least, I'd suggest you to rewrite your function as it follows:

template<class TAnimal>
std::enable_if_t<std::is_base_of<IAnimal, TAnimal>::value>
listenTo(TAnimal const & a ) {
    std::cout << a.sound() << std::endl;
}

That said, as it stands, you don't really need to use neither std::enable_if_t nor any other sfinae expression.
In this case, a static_assert is more than enough:

template<class TAnimal>
void listenTo(TAnimal const & a ) {
    static_assert(std::is_base_of<IAnimal, TAnimal>::value, "!");
    std::cout << a.sound() << std::endl;
}

This way you can also remove the useless definition of sound from IAnimal and still you'll have a nice compilation error.


Now, if you want to drop also the IAnimal interface, a possible solution (that hasn't been mentioned by other answer) follows:

#include <iostream>
#include <type_traits>

template<typename> struct tag {};
template<typename... T> struct check;

template<typename T, typename... U>
struct check<T, U...>: check<U...> {
    using check<U...>::verify;
    static constexpr bool verify(tag<T>) { return true; }
};

template<>
struct check<> {
    template<typename T>
    static constexpr bool verify(tag<T>) { return false; }
};

class Cat {
public:
    std::string sound() const { return std::string("Meow"); }
};

class WildCat {
public:
     std::string sound() const { return std::string("Rarr"); }
};

class Dog {
public:
    std::string sound() const { return std::string("Wuff"); }
};

class Car {
public:
    std::string sound() const { return std::string("Brum"); }
};

using AnimalCheck = check<Cat, WildCat, Dog>;

template<class TAnimal>
void listenTo(TAnimal const & a ) {
    static_assert(AnimalCheck::verify(tag<TAnimal>{}), "!");
    std::cout << a.sound() << std::endl;
}

int main(){
    Cat cat;
    WildCat wildCat;
    Dog dog;
    Car car;

    listenTo(cat);
    listenTo(wildCat);
    listenTo(dog);

    // A car is no animal -> compile time error
    //listenTo(car);

    return 0;
}

As requested in the comments, you can centralize the check of the existence of the method to be called in the check class.
As an example:

template<typename T, typename... U>
struct check<T, U...>: check<U...> {
    static constexpr auto test()
    -> decltype(std::declval<T>().sound(), bool{})
    { return true; }

    static_assert(test(), "!");

    using check<U...>::verify;
    static constexpr bool verify(tag<T>) { return true; }
};

Or a more compact version:

template<typename T, typename... U>
struct check<T, U...>: check<U...> {
    static_assert(decltype(std::declval<T>().sound(), std::true_type{}){}, "!");

    using check<U...>::verify;
    static constexpr bool verify(tag<T>) { return true; }
};

This is somehow a way of checking for a concept by using only features from the current revision of the language.
Note that concepts would have helped to do the same somehow and somewhere in the code, but they are not part of the standard yet.

skypjack
  • 49,335
  • 19
  • 95
  • 187
  • Thanks for your solution. I have the problem that I need to have a look at the definition of the listenTo function to figure out the requirements of the type TAnimal. Do you have an idea to improve it regarding the requirements ? – erikzenker Sep 21 '16 at 22:10
  • @erikzenker I suspect I didn't understand the question in the comment. What's exactly problem? You have to look at `listenTo` for... What? I don't get you, sorry. – skypjack Sep 21 '16 at 22:12
  • Well, I will try to explain it slightly different. The function `listenTo` requires the type `TAnimal` to provide the method `sound`. This requirement is "hidden" in the body of this function. Thus, when I want to provide a new type for `listenTo`, then I need to skim through its function body notice that I need to implement `sound` and then implement it. I would like to have some kind of central interface description I could look at (like it is done with pure virtual methods). – erikzenker Sep 21 '16 at 22:22
  • 1
    @erikzenker Added more details to the answer (at the end of). To sum up, you can extend `check` and put there your requirements. – skypjack Sep 22 '16 at 05:43
0

deleteing a function removes it, it doesn't introduce a dependency on it. It says "this class does not have this function". So as far as implementing/annotating an interface goes, it's a bizarre way to achieve the goal. It's a bit like building a full-cockpit F-32 simulator and telling a very confused first test pilot "well we removed all the buttons so you'll know what actually exists in a real plane".

The way interfaces are implemented in C++ is with virtual functions, and you annotate a virtual function as being "pure" (to be implemented) by giving them a body of "0", like this:

struct IFace {
    virtual void sound() = 0;
};

This makes it impossible to create a concrete instance of IFace or any class that derives from it, until you reach a part of the hierarchy where sound() is implemented:

struct IAudible {
    virtual void sound() const = 0;
};

struct Explosion : public IAudible {
    // note the 'override' keyword, optional but helpful
    virtual void sound() const override { std::cout << "Boom\n"; }
};

struct Word : public IAudible {
};

void announce(const IAudible& audible) {
    audible.sound();
}

int main() {
    Explosion e;
    announce(e);
}

Demo here: http://ideone.com/mGnw6o

But if we try to instantiate "Word", we get a compiler error: http://ideone.com/jriyay

prog.cpp: In function 'int main()':
prog.cpp:21:14: error: cannot declare variable 'w' to be of abstract type 'Word'
         Word w;
prog.cpp:11:12: note:   because the following virtual functions are pure within 'Word':
     struct Word : public IAudible {
            ^
prog.cpp:4:22: note:    virtual void IAudible::sound() const
         virtual void sound() const = 0;
kfsone
  • 23,617
  • 2
  • 42
  • 74
  • You are right that it is some kind of strange. I know how pure virtual methods allow to force the implementation of these methods, but as I mentioned I don't want to use pure virtual methods (e.g. because of performance reasons). – erikzenker Sep 21 '16 at 21:51
  • @erikzenker http://stackoverflow.com/a/449832/257645 I've worked on some pretty massive-scale systems and done a lot of perf work, I'm still waiting for the time where I say "Aha! It's this virtual function call!" – kfsone Sep 21 '16 at 21:57
  • Well, unless you're using mid 90s hardware and a matching compiler. – kfsone Sep 21 '16 at 21:58
  • The link you have provided says it clear: virtual methods have a performance impact, especially when they are called very often. – erikzenker Sep 21 '16 at 22:06