使用不同的enable_if条件选择成员函数

33
我正在尝试根据类模板参数确定调用哪个成员函数的版本。我已经尝试了这个方法:
#include <iostream>
#include <type_traits>

template<typename T>
struct Point
{
  void MyFunction(typename std::enable_if<std::is_same<T, int>::value, T >::type* = 0)
  {
    std::cout << "T is int." << std::endl;
  }

  void MyFunction(typename std::enable_if<!std::is_same<T, int>::value, float >::type* = 0)
  {
    std::cout << "T is not int." << std::endl;
  }
};

int main()
{
  Point<int> intPoint;
  intPoint.MyFunction();

  Point<float> floatPoint;
  floatPoint.MyFunction();
}

我原以为这是在说“如果T是int,则使用第一个MyFunction,如果T不是int,则使用第二个MyFunction”,但是我得到了编译器错误,错误信息是“错误:在‘struct std::enable_if’中没有名为‘type’的类型”。有人能指出我在这里做错了什么吗?

相关问答:"我的 SFINAE 怎么了"(更新版) - HostileFork says dont trust SE
更新:C++20将允许template<typename T> struct Point { void MyFunction() requires (std::is_same_v<T, int>); void MyFunction() requires (!std::is_same_v<T, int>); };。 在这里,约束表达式为非相关且false是可以的-这只是在重载决议过程中使整个函数不那么优先考虑。 - aschepler
6个回答

39
"enable_if" 能够起作用是因为模板参数替换导致错误, 因此该替换从重载决议集中删除,编译器只考虑其他可行的重载。
在您的示例中,在实例化成员函数时没有发生任何替换,因为模板参数 "T" 在那时已知。实现您尝试的最简单方法是创建一个虚拟的模板参数,并将其默认设置为 "T",然后使用它来执行 SFINAE。"
template<typename T>
struct Point
{
  template<typename U = T>
  typename std::enable_if<std::is_same<U, int>::value>::type
    MyFunction()
  {
    std::cout << "T is int." << std::endl;
  }

  template<typename U = T>
  typename std::enable_if<std::is_same<U, float>::value>::type
    MyFunction()
  {
    std::cout << "T is not int." << std::endl;
  }
};

编辑:

正如HostileFork在评论中提到的那样,原始示例留下了用户明确指定成员函数模板参数并获得不正确结果的可能性。以下内容应该防止编译成员函数的显式特化。

template<typename T>
struct Point
{
  template<typename... Dummy, typename U = T>
  typename std::enable_if<std::is_same<U, int>::value>::type
    MyFunction()
  {
    static_assert(sizeof...(Dummy)==0, "Do not specify template arguments!");
    std::cout << "T is int." << std::endl;
  }

  template<typename... Dummy, typename U = T>
  typename std::enable_if<std::is_same<U, float>::value>::type
    MyFunction()
  {
    static_assert(sizeof...(Dummy)==0, "Do not specify template arguments!");
    std::cout << "T is not int." << std::endl;
  }
};

此外,如果有人明确地进行了专门化并且没有使用默认值,那么就没有什么阻止他们进行混合和匹配。因此,你会遇到像 intPoint.MyFunction<float>() 这样的情况,这可能是不正确的。在函数体中加入一个静态断言,以确保 T 与你测试 U 的相同类型匹配,也是必要的。 :-/ - HostileFork says dont trust SE
@HostileFork 说得好,我添加了另一个例子,使用 static_assert 来防止对成员模板的显式特化。 - Praetorian
6
C++11标准是否明确规定不能再这样做了?我认为这将是一个相当大胆的破坏性变化,而且我不知道他们为什么要这么做。如果这是真的,那么现在惯用的 template<class T> auto f(T& v) -> decltype(v.foo()); SFINAE 构造将不能正常工作。 - Xeo
5
@Nawaz表示,这并不意味着在返回类型中进行替换不再是一个软错误。那个线程中的问题是,在meta<int>内部会出现错误,而使用enable_if则不会出现此类错误。 - Xeo
2
@Praetorian 或许你可以添加 static_assert(std::is_same<U, T>::value, "不要指定模板参数!"); 来中断编译。可变参数只是为了提供更多信息的错误消息(其中包含正确的类型)。 - sdd
显示剩余9条评论

6
一个简单的解决方案是使用委派到工作人员的私有函数:
template<typename T>
struct Point
{

  void MyFunction()
  {
     worker(static_cast<T*>(nullptr)); //pass null argument of type T*
  }

private:

  void worker(int*)
  {
    std::cout << "T is int." << std::endl;
  }

  template<typename U>
  void worker(U*)
  {
    std::cout << "T is not int." << std::endl;
  }
};

Tint 时,将调用第一个名为 worker 的函数,因为 static_cast<T*>(0) 会变成类型为 int*。在其他所有情况下,将调用模板版本的 worker 函数。


1
我实际上很喜欢这个。虽然我不认为我会用到它,但是楼主的例子太牵强了,所以这确实是一个好的解决方案。 - Kerrek SB
3
static_cast<T*>(nullptr) - Janek Olszak

3

我认为这个解决方案与@Praetorian的解决方案相同,但我觉得更简单:

template<typename T>
struct Point
{
    template<typename U = T>
    std::enable_if_t<std::is_same<U, T>::value && std::is_same<T, int>::value>
    MyFunction()
    {
        std::cout << "T is int." << std::endl;
    }

    template<typename U = T>
    std::enable_if_t<std::is_same<U, T>::value && std::is_same<T, float>::value>
    MyFunction()
    {
        std::cout << "T is not int." << std::endl;
    }
};

1

enable_if 仅适用于推导的函数模板参数或专门化的类模板参数。你正在做的事情不起作用,因为显然使用固定的 T = int,第二个声明只是错误的。

这是如何完成的:

template <typename T>
void MyFreeFunction(Point<T> const & p,
                    typename std::enable_if<std::is_same<T, int>::value>::type * = nullptr)
{
    std::cout << "T is int" << std::endl;
}

// etc.

int main()
{
    Point<int> ip;
    MyFreeFunction(ip);
}

另一种选择是为各种类型 T 专门化 Point,或将上述自由函数放入嵌套成员模板包装器中(这可能是更“适当”的解决方案)。


我看过这个解决方案,但它似乎真的破坏了代码的可读性。 - David Doria
@DavidDoria:原始代码过于牵强,难以提出更适合的建议。 - Kerrek SB
如果您的情况是仅使用SFINAE来检查某些类型是否为“is_same”(然后在这些类型不匹配时具有默认值),那么为这些固定类型模板专门化“Point”将正是您想要的。您将拥有相同的实例化,但定义更易读。 - HostileFork says dont trust SE

1

根据Praetorian的建议(但不改变函数的返回类型),这似乎可以工作:

#include <iostream>
#include <type_traits>

template<typename T>
struct Point
{
  template<typename U = T>
  void MyFunction(typename std::enable_if<std::is_same<U, int>::value, U >::type* = 0)
  {
    std::cout << "T is int." << std::endl;
  }

  template<typename U = T>
  void MyFunction(typename std::enable_if<!std::is_same<U, int>::value, float >::type* = 0)
  {
    std::cout << "T is not int." << std::endl;
  }
};

int main()
{
  Point<int> intPoint;
  intPoint.MyFunction();

  Point<float> floatPoint;
  floatPoint.MyFunction();
}

从技术上讲,Praetorian的建议并没有改变返回类型,而是改变了参数类型(即通过删除默认参数)。 - Pharap

0

下面的点模板只能使用int或float作为模板参数T进行实例化。

回答这个问题:worker()在方法()调用的模板参数确定后被调用,但你仍然可以控制类型。

    template<typename T>
    struct Point
    {
        static_assert (
              std::is_same<T, int>()  ||
              std::is_same<T, float>()
            );

        template<typename U>
        void method(U x_, U y_)
        {
            if constexpr (std::is_same<T, U>()) {
                worker(x_, y_);
                return;
            }
            // else 
            worker(
                static_cast<T>(x_),
                static_cast<T>(y_)
            );
            return ;
        }


    private:

        mutable T x{}, y{};

        void worker(T x_, T y_)
        {
            // nothing but T x, T y
        }

    };

当然,即使将worker()声明为静态的,它也能正常工作。出于某些有效的原因,上述内容还有其他一些可能的(而且简单的)扩展,但让我们坚持答案。


这个问题特别标记了C++11,if constexpr只能从C++17开始使用。而且mutable不是必需的,通常也不是一个好主意,因为mutable通常会导致问题。 - Pharap

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接