Конструкция "type-switch" в С++ 11

Я все время делаю что-то вроде этого:

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

Как только я вижу закрытие на С++ 11, интересно, что-то вроде этого возможно?

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

Реализация typecase должна быть простой, но я все время сталкиваюсь с проблемой, когда он не может определить аргумент функции, поэтому он не может знать, к чему пытаться использовать dynamic_cast, потому что трудно выводить параметры lambdas. Взял несколько дней на поиск google и SO, но, наконец, понял, поэтому я поделился своим ответом ниже.

Ответ 1

Благодаря ответу от ecatmur на fooobar.com/questions/110296/... мне удалось извлечь подпись из лямбды. Полное решение выглядит следующим образом:

// Begin ecatmur 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 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

и пример использования здесь:

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;
}

Если у кого есть какие-либо улучшения, скажите, пожалуйста!

Ответ 2

Некоторое время назад я экспериментировал, чтобы написать библиотеку для выполнения именно этого.

Вы можете найти его здесь:

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

Проект был довольно амбициозным, с множеством запланированных функций, и его все еще нужно закончить (также есть важная ошибка, о которой я уже знаю, но у меня нет времени работать над этим больше в этом месяце), Однако, для вашего случая использования классической иерархии классов, он будет работать отлично (я думаю).

Основной механизм такой же, как и ранее, но я пытаюсь расширить концепцию с помощью дополнительных функций:

  • Вы можете предоставить случай по умолчанию, который вызывается, если никакое другое предложение не соответствует.
  • Вы можете вернуть значение из предложений. Тип возврата T - это общий тип типов, возвращаемых всеми предложениями. Если нет случая по умолчанию, тип возврата optional<T> вместо T.
  • Вы можете выбрать boost::optional или любую реализацию std::experimental::optional из текущего черновика TS (например, libc++).
  • Вы можете сразу сопоставить несколько параметров.
  • Вы можете сопоставить тип, содержащийся в объекте boost::any.
  • Вы можете подключить переключатель типа с помощью настраиваемого механизма литья, чтобы переопределить dynamic_cast. Это полезно при использовании библиотек, которые предоставляют свою собственную инфраструктуру литья, например Qt qobject_cast, или для реализации типаwitch в ваших собственных тегированных объединениях или что-то в этом роде.

Библиотека полностью завершена, за исключением ошибки, которая задокументирована в README, что делает невозможным сопоставление неполиморфных типов со статическими правилами разрешения перегрузки, но это случай, который был бы полезен только в шаблоном общем коде, и не участвует в большинстве случаев использования, подобных вашим. В настоящее время у меня нет времени работать над этим, чтобы исправить ошибку, но я полагаю, что лучше разместить ее здесь, чем оставить ее неиспользованной.

Ответ 3

Реализация

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);
    }
}

Использование

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

У меня нет доступа к компилятору С++ 11, чтобы попробовать это, но я надеюсь, что эта идея полезна. В зависимости от того, сколько экземпляров вывода, на которые способен компилятор, вам может потребоваться или не обязательно указывать тип case дважды - я не С++ 11-pro достаточно, чтобы сказать, глядя на него.

Ответ 4

Я думаю, это зависит от того, хотите ли вы сделать это во время компиляции или во время выполнения. Для времени компиляции ответ Вердагона лучше, во время выполнения вы можете сделать что-то вроде этого

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)]();
}

но оба пути неверны, ключевое слово virtual здесь для этого, вы ДОЛЖНЫ делать, как сказал Ари, или в вашей архитектуре есть ошибка.

Ответ 5

Я думаю, вы действительно хотите использовать наследование, а не typecasing (я никогда не видел его раньше, довольно аккуратно:)) или проверки типов.

Небольшой пример:

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!"
}

В этом случае я хотел напечатать звук, специфичный для животных, без специальной проверки типа. Для этого я использую виртуальную функцию "makeSound" для каждого подкласса Animal имеет и отменяет печать правильного вывода для этого животного.

Надеюсь, это то, к чему вы стремились.