基于方法的特化模板

Specializing templates based on methods

最近我在 Java 中进行了很多编程,现在我回到了我的 C++ 根源(我真的开始想念指针和分段错误)。知道 C++ 对模板有广泛的支持后,我想知道它是否具有 Java 的某些功能,这对编写通用代码很有用。假设我有两组 classes。其中一个具有 first() 方法,另一个具有 second() 方法。有没有一种方法可以根据 class 拥有的方法专门化编译器选择的模板?我的目标是类似于 Java:

的行为
public class Main {
    public static void main(String[] args) {
        First first = () -> System.out.println("first");
        Second second = () -> System.out.println("second");
        method(first);
        method(second);
    }

    static <T extends First> void method(T argument) {
        argument.first();   
    }

    static <T extends Second> void method(T argument) {
        argument.second();
    }
}

其中 FirstSecond 是接口。我知道我可以通过从上层 class 导出它们中的每一个来对这两个组进行分组,但这并不总是可能的(C++ 中没有自动装箱和一些 classes 不继承自共同祖先) .

我的需求的一个很好的例子是 STL 库,其中一些 classes 具有 push() 等方法,而另一些则具有 insert()push_back()。假设我想创建一个函数,该函数必须使用可变参数函数将多个值插入到容器中。在 Java 中很容易执行,因为集合有一个共同的祖先。另一方面,在 C++ 中,情况并非总是如此。我通过 duck-typing 尝试了它,但编译器产生了一条错误消息:

template <typename T>
void generic_fcn(T argument) {
    argument.first();
}

template <typename T>
void generic_fcn(T argument) {
    argument.second();
}

所以我的问题是:是否可以通过专门化每个案例来实现这种行为而不创建不必要的样板代码?

而不是 <T extends First>,您将使用我们称为 sfinae 的东西。这是一种根据参数类型在函数上添加约束的技术。

以下是你在 C++ 中的做法:

template <typename T>
auto generic_fcn(T argument) -> void_t<decltype(argument.first())> {
    argument.first();
}

template <typename T>
auto generic_fcn(T argument) -> void_t<decltype(argument.second())> {
    argument.second();
}

要使函数存在,编译器需要 argument.second()argument.first() 的类型。如果表达式没有产生类型(即 T 没有 first() 函数),编译器将尝试另一个重载。

void_t实现如下:

template<typename...>
using void_t = void;

另一个很棒的事情是,如果你有这样的 class:

struct Bummer {
    void first() {}
    void second() {}
};

然后编译器会有效地告诉您调用是不明确的,因为类型匹配两个约束。


如果你真的想测试一个类型是否扩展了另一个(或实现,在 C++ 中是一样的)你可以使用类型特征 std::is_base_of

template <typename T>
auto generic_fcn(T argument) -> std::enable_if_t<std::is_base_of<First, T>::value> {
    argument.first();
}

template <typename T>
auto generic_fcn(T argument) -> std::enable_if_t<std::is_base_of<Second, T>::value> {
    argument.second();
}

要阅读有关此主题的更多信息,请查看标准库提供的 sfinae on cpprefence, and you can check available traits

您可以按如下方式调度呼叫:

#include<utility>
#include<iostream>

struct S {
    template<typename T>
    auto func(int) -> decltype(std::declval<T>().first(), void())
    { std::cout << "first" << std::endl; }

    template<typename T>
    auto func(char) -> decltype(std::declval<T>().second(), void())
    { std::cout << "second" << std::endl; }

    template<typename T>
    auto func() { return func<T>(0); }
};

struct First {
    void first() {}
};

struct Second {
    void second() {}
};

int main() {
    S s;
    s.func<First>();
    s.func<Second>();
}

方法 first 优于 second 如果 class 两者都有。
否则,func使用函数重载来测试两种方法并选择正确的方法。
此技术称为 sfinae,请使用此名称在网络上搜索以获取更多详细信息。

C++ 中可用的选项太多了。

我的偏好是支持自由函数和 return 正确的任何结果类型。

#include <utility>
#include <type_traits>
#include <iostream>

struct X
{
  int first() { return 1; }
};

struct Y
{
  double second() { return 2.2; }
};


//
// option 1 - specific overloads
//

decltype(auto) generic_function(X& x) { return x.first(); }
decltype(auto) generic_function(Y& y) { return y.second(); }

//
// option 2 - enable_if
//

namespace detail {
  template<class T> struct has_member_first
  {
    template<class U> static auto test(U*p) -> decltype(p->first(), void(), std::true_type());
    static auto test(...) -> decltype(std::false_type());
    using type = decltype(test(static_cast<T*>(nullptr)));
  };
}
template<class T> using has_member_first = typename detail::has_member_first<T>::type;

namespace detail {
  template<class T> struct has_member_second
  {
    template<class U> static auto test(U*p) -> decltype(p->second(), void(), std::true_type());
    static auto test(...) -> decltype(std::false_type());
    using type = decltype(test(static_cast<T*>(nullptr)));
  };
}
template<class T> using has_member_second = typename detail::has_member_second<T>::type;

template<class T, std::enable_if_t<has_member_first<T>::value>* =nullptr> 
decltype(auto) generic_func2(T& t)
{
  return t.first();
}

template<class T, std::enable_if_t<has_member_second<T>::value>* =nullptr> 
decltype(auto) generic_func2(T& t)
{
  return t.second();
}

//
// option 3 - SFNAE with simple decltype
//

template<class T>
auto generic_func3(T&t) -> decltype(t.first())
{
  return t.first();
}

template<class T>
auto generic_func3(T&t) -> decltype(t.second())
{
  return t.second();
}


int main()
{
  X x;
  Y y;

  std::cout << generic_function(x) << std::endl;
  std::cout << generic_function(y) << std::endl;

  std::cout << generic_func2(x) << std::endl;
  std::cout << generic_func2(y) << std::endl;

  std::cout << generic_func3(x) << std::endl;
  std::cout << generic_func3(y) << std::endl;

}

这里有一个小库,可以帮助您确定成员是否存在。

namespace details {
  template<template<class...>class Z, class always_void, class...>
  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...>;

现在我们可以轻松写出 has first 和 has second:

template<class T>
using first_result = decltype(std::declval<T>().first());
template<class T>
using has_first = can_apply<first_result, T>;

second.

也类似

现在我们有了我们的方法。我们想调用第一个或第二个。

template<class T>
void method_second( T& t, std::true_type has_second ) {
  t.second();
}
template<class T>
void method_first( T& t, std::false_type has_first ) = delete; // error message
template<class T>
void method_first( T& t, std::true_type has_first ) {
  t.first();
}
template<class T>
void method_first( T& t, std::false_type has_first ) {
  method_second( t, has_second<T&>{} );
}
template<class T>
void method( T& t ) {
  method_first( t, has_first<T&>{} );
}

这称为标签分派。

method 调用 method_first 确定 T& 是否可以用 .first() 调用。如果可以,它会调用调用 .first().

的那个

如果不能,它会调用转发到 method_second 的那个并测试它是否有 .second()

如果两者都没有,它会调用一个 =delete 函数,该函数会在编译时生成一条错误消息。

有很多很多方法可以做到这一点。我个人喜欢标签调度,因为与 SFIANE 生成的错误消息相比,您可以从匹配失败中获得更好的错误消息。

在C++17中你可以更直接:

template<class T>
void method(T & t) {
  if constexpr (has_first<T&>{}) {
    t.first();
  }
  if constexpr (has_second<T&>{}) {
    t.second();
  }
}