4

在我自己的代码中出现错误之后,(在我看来)编译器选择了错误的重载,我一直在寻找解释,但找不到一个简单的解释。我确实找到了处理专业化问题的Herb Sutter 的 GOTW 49。我还在stackoverflow上发现了一些问题,但没有一个能真正向我解释原因,也不能为我提供好的解决方案。

我有一个类 Foo,它可以从布尔值构造。我发现(很难)std::string 也可以从 bool (假)构造。

我有三个具有不同参数的(模板)方法,如下所示。一个方法接受“任何”模板参数,两个特化,接受一个struct Foo,另一个接受一个字符串。

#include <string>
#include <iostream>

struct Foo
{
    Foo() : value( false ){ };
    Foo( bool v ) : value ( v ) { } 
    Foo( const bool& v ) : value( v ) { }

    bool value;
};

template< typename T >
void bar( const T& value )
{
    std::cerr << "template bar" <<  std::endl;
}

template< >
void bar< Foo >( const Foo& )
{
    std::cerr << "template bar with Foo" << std::endl;
}

template< typename T >
void bar( const std::string& )
{
    std::cerr << "template bar with string" << std::endl;
}

int main( int argc, char* argv[] )
{
    bar( false ); // Succeeds and calls 1st bar( const T& )
    bar< Foo >( false ); // Crashes, because 2nd bar( const std::string& )
                         // is called with false promoted to null pointer.

    return 0;
}

我已经使用 Visual Studio 2010 和 MinGW (gcc 4.7.0) 对此进行了测试。GCC 很好地给出了编译警告,但 msvc 没有:

main.cpp:34:20: warning: converting 'false' to pointer type for argument 1 of 'std::basic_string< ... ' [-Wconversion-null]

小更新(在代码中):即使是 Foo 的显式特化也不起作用。\

小更新 2:编译器不会抱怨“模棱两可的重载”。

小更新 3:有人回答说 Foo 的两个构造函数接受 a bool,“使”Foo 的选择“无效”。我只用一个转换构造函数测试了类似的版本。这些也不起作用。

问题:

  1. 为什么编译器会尝试调用字符串参数版本?
  2. 以及为什么添加<Foo>bar() 调用很重要。
  3. 我怎样才能防止这种情况发生。例如,我可以强制编译器在bar( const Foo& )输入布尔值时进行选择吗?
  4. 或者,当有人调用时,我可以强制执行编译错误bar< Foo >( false )吗?
4

3 回答 3

4

以下是四个问题的答案:

  1. 为什么编译器会尝试调用字符串参数版本?您的类Foo有两个构造函数,bool它们采用模棱两可的 a,因此不考虑转换boolFoo(如果您想检测是否传入了临时值或左值,您可以在 C++ 中使用bool&&bool const&作为参数类型重载)。std::string可以从 a 构造char const*并且false可以提升为空指针常量。空指针常量在语法上是有效的char const*,但它是非法值,传递它会导致未定义的行为。
  2. 为什么在 bar() 调用中的添加很重要?指定模板参数时,您禁止模板参数推导并告诉编译器使用哪个参数。显式指定模板参数是可以选择bar()采用 a的重载的唯一方法,因为无法为此模板推导。std::stringT
  3. 我怎样才能防止这种情况发生。例如,当输入 bool 时,我可以强制编译器选择 bar( const Foo& ) 吗?最简单的方法是让编译器推断模板参数:bar()编译器永远不会自动选择 s 的版本,因为编译器无法推断模板参数。或者,如果您想显式指定模板参数并且只是bool一个问题,您可以添加一个重载bool,并使推导的版本和bool版本委托给相同的内部函数。
  4. 或者,当有人调用 bar< Foo >( false ) 时,我可以强制执行编译错误吗?通过删除重载(见下文),这将很容易使用 C++ 2011。

bool要在使用显式指定的模板参数时创建编译时错误,您可以添加此重载:

template <typename T> void bar(bool) = delete;

删除功能在 C++ 2011 中可用。

主要问题似乎是:如果Foostd::string都可以从 转换bool,为什么std::string选择转换 ifbar<Foo>(bool)被调用并且以下重载可用?

template <typename T> void bar(T const&);
template <>           void bar<Foo>(Foo const&);
template <typename T> void bar(std::string const&);

首先,重载决议选择主模板,忽略任何特化)。由于bar(std::string const&)作为比推导模板参数的版本更专业的接口,因此选择了此版本。在这个阶段忽略第一个模板的特化。要使用也Foo适用,您可以添加

template <typename T> void bar(Foo const&);

和调用bar<Foo>(false)将是std::stringFoo版本之间的歧义。

于 2012-10-19T15:06:58.147 回答
2

第一个示例bar( false );调用 ,template<typename T> void bar( const T& value )因为它与 T = bool 完全匹配。

当您指定 T = Foo 时,这两个重载都不再是完全匹配的,因此您将进入关于应用哪些隐式转换的相当复杂的规则。大多数 C++ 程序员并不完全理解这些,所以最好避免隐式转换。

在这种情况下,最简单的解决方法是为 bool 添加另一个重载。

template<typename T>
void bar( bool )
{
    std::cerr << "bool" << std::endl;
}

然后在该重载中,您可以显式应用转换并调用所需的版本。

于 2012-10-19T15:01:30.260 回答
1

这条线

bar< Foo >( false );

可以匹配以下两个函数中的任何一个:

void bar<Foo>(const Foo&);
void bar<Foo>(const std::string&);

现在,如果两个 UDT 都有来自 的隐式转换构造函数bool,它应该选择哪一个?它们都具有相同的优先级,因为它们都是相同类型的转换。

至少......大概就是这样,尽管它真的不应该选择提升+构建而不是仅构建序列。

于 2012-10-19T14:58:50.760 回答