为什么我们在使用宏时找不到合适的运算符重载?

Why do we not find the right operator overload when using the macro?

我正在编写一个 class 模板,它采用任意函数指针作为非类型模板参数。我想用

template <auto F> struct Foo;

但我的编译器 (MSVC 2017.5) 不支持模板参数列表中的 auto(即使它支持许多 C++17 功能)。所以我以某种方式围绕这个写了一个 hack:

template <typename T>
using Func = void (*)(T);

template <typename TF, TF F> struct Foo;
template <typename T, Func<T> F> 
struct Foo<Func<T>, F> { ... };

#define FOO(func) Foo<decltype(func), func>

我实现了一个流式运算符(用于 QDebug 或任何其他文本流),如下所示:

template <typename T, Func<T> F>
QDebug &operator <<(QDebug &out, const FOO(F) &foo)
{
    ...
    return out;
}

但是我的主代码找不到正确的operator<<重载:

void func(int) { ... }

...

FOO(&func) foo;
qDebug() << foo; // error

令人惊讶的是,定义运算符时一切正常

QDebug &operator <<(QDebug &out, const Foo<Func<T>, F> &foo)
//                                     ^^^^^^^^^^^^^^^

最后一行中的 Func<T> 和宏中的 decltype<F> 似乎没有给出相同的类型。但是我检查了这个并且 std::is_same_v<Func<int>, decltype(&func)> 给出了 true。我想不出为什么使用宏 FOO 会给我任何其他编译时行为,就像不使用它时一样。


一个最小的工作示例:

#include <iostream>

template <typename T>
using Func = void (*)(T);

template <typename TF, TF F> struct Foo;
template <typename T, Func<T> F> 
struct Foo<Func<T>, F> { };

#define FOO(func) Foo<decltype(func), func>

template <typename T, Func<T> F>
std::ostream &operator <<(std::ostream &out, const FOO(F) &foo)
// std::ostream &operator <<(std::ostream &out, const Foo<Func<T>,F> &foo)
{
    return out;
}

void func(int);

int main(int argc, char **argv)
{
    FOO(&func) foo;
    std::cout << foo << std::endl; // error
}

当没有使用宏时,第二个模板参数 F 可以从第二个函数参数 foo 推导出来。当使用宏时,第二个模板参数 F 不能从第二个函数参数中推导出来,因为它会出现在 decltype 内部:Foo<decltype(F), F> & foo。您的代码可以简化为

template<typename T>
void f(decltype(T) v){}

int v{};
f(v);

编译器知道参数的类型 (int) 但是模板参数 T 不能从已知的参数类型推导出来,因为在 decltype T 内部使用时必须提前知道。

解决方法:

template <typename T, Func<T> F> 
struct Foo<Func<T>, F> {
  friend QDebug &operator <<(QDebug &out, const Foo &foo){
    ...
    return out;
  }
};

作为 template auto paper, we also got a new deduction rule in [temp.deduct.type] 的一部分:

When the value of the argument corresponding to a non-type template parameter P that is declared with a dependent type is deduced from an expression, the template parameters in the type of P are deduced from the type of the value.

此规则允许以下示例在 C++17 中工作,因为我们可以从 V 的类型推导出 T:

template <typename T, T V>
struct constant { };

template <typename T, T V>
void foo(constant<decltype(V), V> ) { }

int main() {
    foo(constant<int, 4>{});
}

在 C++14 及更早版本中,此示例格式错误,因为未推导 T。当您使用该宏时,您正试图(隐含地)使用这种行为,它扩展为:

template <typename T, Func<T> F>
std::ostream &operator <<(std::ostream &out, const Foo<decltype(F), F> &foo);

您正试图从 F 中推导出 T。由于 MSVC 尚不支持 template auto,因此它也不支持使 template auto 工作所需的机器的其他部分,这也许不足为奇。这就是宏和非宏替代方案之间的区别,可以简单地推断出 T:

template <typename T, Func<T> F>
std::ostream &operator <<(std::ostream &out, const Foo<Func<T>,F> &foo)

所以简单的解决方案就是...不要使用宏,因为您有一个有效的表单,即使它更冗长。一个更长的解决方案是使用 完全回避整个推导问题。