"type-switch" construct in C++11
Asked Answered
H

6

15

All the time, I find myself doing something like this:

Animal *animal = ...
if (Cat *cat = dynamic_cast<Cat *>(animal)) {
    ...
}
else if (Dog *dog = dynamic_cast<Dog *>(animal)) {
    ...
}
else { assert(false); }

Once I see closures in C++11, I wonder, is something like this possible?

Animal *animal = ...
typecase(animal,
    [](Cat *cat) {
        ...
    },
    [](Dog *dog) {
        ...
    });

Implementing typecase should have been easy, but I keep running into a problem where it can't figure out the function's argument, so it can't know what to try to dynamic_cast to, because it's hard to deduce lambdas' parameters. Took a few days of searching google and SO, but finally figured it out, so I'll share my answer below.

Hadleigh answered 2/4, 2014 at 20:54 Comment(13)
My advice is: Don't do that! What's wrong with polymorphism and virtual functions? If you need to do like that it's almost always a sign of bad design.Bioscopy
If you have to provide special code outside your classes for each derived type, you have a very dirty design. The sense of objects is to hide implementation details inside the class and have a unique interface. You do the opposite!Melessa
@JoachimPileborg There are situations where it makes sense to do this. If the per-class behavior is local to the place where the class hierarchy is being used instead of being intrinsic to those classes, it is useful to be able to switch on types like this. Functional programming languages typically have a match keyword for exactly this purpose.Pail
@TimothyShields I agree that in a few (very few) cases there may be no way around doing it like this. But to quote the OP: "All the time, I find myself doing something like this..." (emphasis mine) It would seem that the OP is using this pattern much more than actually needed.Bioscopy
@JoachimPileborg I'll agree with that.Pail
Perhaps, perhaps not. For this example with the cat and dog, I'm definitely misusing the pattern, if the action is makeSound() for example. In one of my projects now, I'm passing messages (UnitMovedMessage, UnitAttackedMessage, PlayerQuitMessage) and I definitely don't want the logic to be inside the message... I don't want server code, which manipulates massive swaths of state, to be inside the message class, which can be called by anyone with the message. I like the functional approach here.Hadleigh
@verdagon If the message differs by args and type, and the code difference is in the server, you are still doing it wrong if you are using dynamic_cast all over the place. It is like using a full steel foundary in order to make penny nails.Khajeh
Perhaps I shouldn't have said server; it's passing messages between threads, which means I can use the vptr to figure out what type it is. It makes sense to me because the messages were already virtual, for other memory-management reasons. With all of these things, it makes sense to me to dynamic_cast, so I don't have to introduce another enum which has to be maintained. I could very well be wrong though, this is a new area of C++ to me. Thoughts?Hadleigh
Adding a comment even if after a long time. To all the people who say this is bad design, are you sure? Anybody who has written code in functional languages with pattern patching on constructors have familiarity with such a construct. Even Bjarne Stroustrup has recently published a library that does exactly that thing in a very clever and efficient way, and in the paper "Open and Efficient Type Switch for C++" highlights the benefits of such a construct over the Visitor pattern. There also will be a talk named "Accept no Visitors" at the next CppCon that goes over exactly the same issuesBristling
@gigabytes I guess my issue is on two fronts. On one front, you should know what your objects are and how they behave, that is what interfaces are for. Dynamic cast implies your interfaces are inadequate. On another front, the C++ built in type system is a bit crippled, if you want something as powerful as pattern matching, don't shoe-horn in the little bit of it you get with serial calls to dynamic cast in C++. Write a type system that does it well. Think about it: the "switch" does a linear search over the cases! There isn't any good reason for that, other than the crippled dynamic cast.Khajeh
@Yakk You are assuming the only way to implement a typeswitch is through inheritance and dynamic casts. Generic programming design and functional-style algorithms and data structure are actually a great fit in C++, you don't need a different type system. In this context, composition is preferred over inheritance, and pattern matching constructs are the natural way to treat data. Very fast type switches can be implemented (see Bjarne's Mach7 library), inheritance is not the way to go. And if you accept closed data types (ADTs in functional languages are closed) you get O(1) switches for free.Bristling
@gigabytes Sure: my problem (on that front) is with dynamic_cast, not with typeswitches: dyanmic_cast based typeswitches are a bad idea. The other front is that you probably should publish contract details somewhere, and if you are using inheritance the usual spot is in the interface: other places for contracts (especially if enforced reasonably) are acceptable. The OP, however, is using inheritance and interfaces, so on both fronts the typeswitch is highly questionable.Khajeh
@Yakk it is generally questionable in a canonical OOP codebase. I only wanted to point that this suggestion from OOP (replace downcasts with interface polymorphism) is increasingly becoming less important, especially in C++. It depends on what you are doing. If you look at clang's source code, for example, the traversal of the AST is usually done through a dyn_cast<T>() template function which is essentially a dynamic_cast reimplemented to avoid RTTI-related problems.Bristling
H
12

Thanks to an answer from ecatmur at https://stackoverflow.com/a/13359520 I was able to extract the signature from the lambda. The full solution looks like this:

// Begin ecatmur's code
template<typename T> struct remove_class { };
template<typename C, typename R, typename... A>
struct remove_class<R(C::*)(A...)> { using type = R(A...); };
template<typename C, typename R, typename... A>
struct remove_class<R(C::*)(A...) const> { using type = R(A...); };
template<typename C, typename R, typename... A>
struct remove_class<R(C::*)(A...) volatile> { using type = R(A...); };
template<typename C, typename R, typename... A>
struct remove_class<R(C::*)(A...) const volatile> { using type = R(A...); };

template<typename T>
struct get_signature_impl { using type = typename remove_class<
    decltype(&std::remove_reference<T>::type::operator())>::type; };
template<typename R, typename... A>
struct get_signature_impl<R(A...)> { using type = R(A...); };
template<typename R, typename... A>
struct get_signature_impl<R(&)(A...)> { using type = R(A...); };
template<typename R, typename... A>
struct get_signature_impl<R(*)(A...)> { using type = R(A...); };
template<typename T> using get_signature = typename get_signature_impl<T>::type;
// End ecatmur's code

// Begin typecase code
template<typename Base, typename FirstSubclass, typename... RestOfSubclasses>
void typecase(
        Base *base,
        FirstSubclass &&first,
        RestOfSubclasses &&... rest) {

    using Signature = get_signature<FirstSubclass>;
    using Function = std::function<Signature>;

    if (typecaseHelper(base, (Function)first)) {
        return;
    }
    else {
        typecase(base, rest...);
    }
}
template<typename Base>
void typecase(Base *) {
    assert(false);
}
template<typename Base, typename T>
bool typecaseHelper(Base *base, std::function<void(T *)> func) {
    if (T *first = dynamic_cast<T *>(base)) {
        func(first);
        return true;
    }
    else {
        return false;
    }
}
// End typecase code

and an example usage is here:

class MyBaseClass {
public:
    virtual ~MyBaseClass() { }
};
class MyDerivedA : public MyBaseClass { };
class MyDerivedB : public MyBaseClass { };


int main() {
    MyBaseClass *basePtr = new MyDerivedB();

    typecase(basePtr,
        [](MyDerivedA *a) {
            std::cout << "is type A!" << std::endl;
        },
        [](MyDerivedB *b) {
            std::cout << "is type B!" << std::endl;
        });

    return 0;
}

If anyone has any improvements, please tell me!

Hadleigh answered 2/4, 2014 at 20:54 Comment(6)
Why the assert in the default case? Better allow defining a default action if nothing matches...Madden
Good idea. I only added the assert because I always forget to add clauses to various if-else ladders whenever i add another subclass. java's switches, when used with enums, have lovely warnings for when you forget a case, but this isn't exactly java... Anyway, do you think I should use some sort of no-argument lambda to accomplish it? Ideas?Hadleigh
Hm... After careful consideration, it can already be done now: Last lambda just receives the static type. Should be auto-optimized: dynamic_cast -> static_cast -> no cast. Should the Lambdas be able to use references instead of pointers? As far as i can see, with a little more pixie-dust, that should be possible?Madden
Have a look at "Modern C++ Design" Multimethods 11.3 "Double Switch-on-Type: Brute Force". Andrei Alexandrescu implemeneted multimethods this way. He notes the importance of sorting the derived classes to check the most derived classes first (Page 267). I think you should take this into account for your solution.Solly
@Jan: I hope he also stresses using a stable sort. Kind of embarassing in a multi-inheritance scheme, if the wrong specialisation is called.Madden
This is what I went with, works great. I did write it to use references rather than pointers. Much more pleasing than comparing typeid and casting. Thanks.Incognizant
P
4

Implementation

template <typename T, typename B>
void action_if(B* value, std::function<void(T*)> action)
{
    auto cast_value = dynamic_cast<T*>(value);
    if (cast_value != nullptr)
    {
        action(cast_value);
    }
}

Usage

Animal* animal = ...;
action_if<Cat>(animal, [](Cat* cat)
{
    ...
});
action_if<Dog>(animal, [](Dog* dog)
{
    ...
});

I don't have access to a C++11 compiler right this second to try this out, but I hope the idea is useful. Depending on how much type inference the compiler is capable of, you may or may not have to specify the case's type twice - I'm not C++11-pro enough to tell from looking at it.

Pail answered 2/4, 2014 at 21:33 Comment(2)
I think this is a cool idea, but one of the main benefits of structuring it like a switch-statement is that you can have the assert-false in the default case, at least, it's a benefit to my style of coding. If you don't need that, then your solution is quite delicious indeed!Hadleigh
You could avoid typing Cat and Dog twice by making it template <typename B, typename F> void action_if(B* value, F action) and using boost::function_traits to deduce T from F.Seppuku
A
3

I think it depends if you want do this at compile time or at run time. For compile time, Verdagon's answer is better, at runtime you can do something like this

class A {
};

class B {
};

void doForA() {
    std::cout << "I'm A" << std::endl;
}

void doForB() {
    std::cout << "I'm B" << std::endl;
}

int main() {
    std::unordered_map<std::type_index, std::function<void(void)>> mytypes;
    mytypes[typeid(A)] = doForA;
    mytypes[typeid(B)] = doForB;

    mytypes[typeid(A)]();
}

but both ways are wrong the virtual keyword is here for this, you MUST do like Arie said or there is mistake in your architecture

Apostil answered 2/4, 2014 at 21:52 Comment(0)
B
3

Some time ago I was experimenting to write a library for doing exactly that.

You can find it here:

https://github.com/nicola-gigante/typeswitch

The project was quite ambitious, with a lot of planned features, and it still needs to be finished (there's also an important bug that I already know, but I don't have time to work on it anymore in this months). However, for your use case of a classic hierarchy of classes, it would work perfectly (I think).

The basic mechanism is the same as you have posted before, but I try to extend the concept with more features:

  • You can provide a default case that is called if no other clause matches.
  • You can return a value from the clauses. The return type T is the common type of the types returned by all the clauses. If there's no default case, the return type is optional<T> instead of T.
  • You can choose between boost::optional or any implementation of std::experimental::optional from the current draft of the Library Foundamentals TS (for example, libc++ provides one).
  • You can match multiple parameters at once.
  • You can match the type contained in a boost::any object.
  • You can hook the type switch with a custom casting mechanism, to override the dynamic_cast. This is useful when using libraries that provide their own casting infrastructure, like Qt's qobject_cast, or to implement the typeswitch on your own tagged unions or anything like that.

The library is quite finished, except for a bug that is documented in the README that makes impossible to match non-polymorphic types with static overloading resolution rules, but it's a case that would only be useful in templated generic code, and is not involved in the majority of use cases like yours. I currently don't have time to work on it to fix the bug, but I suppose it's better to post it here than leave it unused.

Bristling answered 25/8, 2014 at 18:50 Comment(0)
M
1

I think you actually want to use inheritance rather than typecasing (I've never seen it before, pretty neat :) ) or type-checking.

Small example:

class Animal {
public:
   virtual void makeSound() = 0; // This is a pure virtual func - no implementation in base class
};

class Dog : public Animal {
public:
   virtual void makeSound() { std::cout << "Woof!" << std::endl; }
}

class Cat : public Animal {
public:
   virtual void makeSound() { std::cout << "Meow!" << std::endl; }
}

int main() {
   Animal* obj = new Cat;

   obj->makeSound(); // This will print "Meow!"
}

In this case, I wanted to print the animal-specific sound without specific type checking. To do so, I use the virtual function "makeSound" each subclass of Animal has and overrides to print the correct output for that animal.

Hope this is what you were aiming for.

Medallist answered 2/4, 2014 at 21:15 Comment(2)
Where's the inheritance?Bioscopy
One must not forget the inheritance when showing how to implement inheritance... Thank you @JoachimPileborg :)Medallist
B
0

A little trick, how to create static information about types so that you can compare them quickly with one switch statically or dynamically.

#include <iostream>
#include <vector>

struct Base {
    int m_ClassID;
    Base(int _id) : m_ClassID(_id) {} // dynamic classID
};

template<int N>
struct Base_ : Base {
    static constexpr int g_ClassID = N; // static classID
    Base_() : Base(N) {}
};

struct A : Base_<2222> // define ClassID
{};

struct B  : Base_<3333> // define ClassID
{};

void DoForAnyBase(Base* pBase) {
    switch(pBase->m_ClassID) {
    case A::g_ClassID:
        printf("I'm A");
        break;
    case B::g_ClassID:
        printf("I'm B");
        break;
    }
}

int main() {
    std::vector< Base* > aTypes;
    aTypes.push_back( new A() );
    aTypes.push_back( new B() );

    for( Base* pCur : aTypes) {
        DoForAnyBase(pCur);
        delete pCur;
    }
}
Bangui answered 29/1, 2023 at 12:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.