Контравариантность для параметра шаблонного обратного вызова, как в C #

Настройка

Рассмотрим два типа, один из которых наследуется от другого:

#include <iostream>

class shape {  };
class circle : Shape {  };

И две функции, которые принимают объект этого типа, соответственно:

void shape_func(const shape& s) { std::cout << "shape_func called!\n"; }
void circle_func(const circle& c) { std::cout << "circle_func called!\n"; }

Шаблон функции

Теперь я хочу шаблон функции, который я могу передать:

  1. Объект одного из этих типов (или другим это нравится).
  2. Указатель на одну из этих функций (или другим это нравится) это совместимо с переданным объектом.

Вот моя попытка объявить этот шаблон функции:

template<class T>
void call_twice(const T& obj, void(*func)(const T&))
{
func(obj);
func(obj);
}

(На практике его тело будет делать что-то более полезное, но для демонстрации я просто позволю ему дважды вызывать переданную функцию с переданным объектом.)

Наблюдаемое поведение

int main() {
shape s;
circle c;

call_twice<shape>(s, &shape_func);   // works fine
call_twice<circle>(c, &circle_func); // works fine
//call_twice<circle>(c, &shape_func);  // compiler error if uncommented
}

Ожидаемое поведение

Я надеялся, что третий звонок тоже сработает.

В конце концов, так как shape_func принимает любой shapeтакже принимает circle — так, подставив circle за T у компилятора должна быть возможность разрешать шаблон функции без конфликтов.

Фактически, именно так ведет себя соответствующая универсальная функция в C #:

// C# code
static void call_twice<T>(T obj, Action<T> func) { ... }

Это можно назвать как call_twice(c, shape_func) без проблем, потому что, говоря в C # lingo, параметр типа T из Action<T> является контравариантный.

Вопрос

Возможно ли это в C ++?

То есть как бы функционировал шаблон call_twice должны быть реализованы, чтобы принять все три вызова в этом примере?

2

Решение

Один из способов сделать это через SFINAE, лучше всего показано на примере:

#include <iostream>
#include <type_traits>

struct Shape {};
struct Circle : public Shape {};

template<class Bp, class Dp>
std::enable_if_t<std::is_base_of<Bp,Dp>::value,void>
call_fn(Dp const& obj, void (*pfn)(const Bp&))
{
pfn(obj);
}

void shape_func(const Shape& s) { std::cout << "shape_func called!\n"; }
void circle_func(const Circle& s) { std::cout << "circle_func called!\n"; }

int main()
{
Shape shape;
Circle circle;

call_fn(shape, shape_func);
call_fn(circle, circle_func);
call_fn(circle, shape_func);
}

Выход

shape_func called!
circle_func called!
shape_func called!

Как это устроено

Эта реализация использует простое (возможно, слишком много) упражнение, использующее std::enable_if в сочетании с std::is_base_of обеспечить квалифицированное разрешение перегрузки потенциально двумя различными типами (один из объектов, другой из списка аргументов функции обеспечения). В частности, это:

template<class Bp, class Dp>
std::enable_if_t<std::is_base_of<Bp,Dp>::value,void>
call_fn(Dp const& obj, void (*pfn)(const Bp&))

говорит, что этот шаблон функции требует двух аргументов шаблона. Если они либо одного типа ИЛИ Bp как-то основа Dpзатем укажите тип (в этом случае void). Затем мы используем этот тип как тип результата нашей функции. Следовательно, для первого вызова результирующая реализация выглядит следующим образом после вычета:

void call_fn(Shape const& obj, void (*pfn)(const Shape&))

это было то, что мы хотели. Аналогичное создание в результате второго вызова:

void call_fn(Circle const& obj, void (*pfn)(const Circle&))

Третий экземпляр создаст это:

void call_fn(Circle const& obj, void (*pfn)(const Shape&))

так как Dp а также Bp разные, но Dp является производной.


Дело об отказе

Чтобы увидеть этот сбой (как мы этого хотим), измените код с не связанными типами. Просто удалить Shape из списка наследования базового класса Circle:

#include <iostream>
#include <type_traits>

struct Shape {};
struct Circle {};

template<class Bp, class Dp>
std::enable_if_t<std::is_base_of<Bp,Dp>::value,void>
call_fn(Dp const& obj, void (*pfn)(const Bp&))
{
pfn(obj);
}

void shape_func(const Shape& s) { std::cout << "shape_func called!\n"; }
void circle_func(const Circle& s) { std::cout << "circle_func called!\n"; }

int main()
{
Shape shape;
Circle circle;

call_fn(shape, shape_func);   // still ok.
call_fn(circle, circle_func); // still ok.
call_fn(circle, shape_func);  // not OK. no overload available,
// since a Circle is not a Shape.
}

Результатом будет несоответствующий вызов функции для третьего вызова.

1

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]