由于错误的转换导致过载/专业化不正确

时间:2012-10-19 14:45:26

标签: c++

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

我有一个单独的类Foo,它可以用布尔值构造。我发现(艰难的方式)std :: string也可以用bool(false)构建。

我有三个(模板)方法,具有不同的参数,如下所示。一个方法接受“任何”模板参数和两个特化,接受一个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的两个构造函数接受bool,“使Foo的选择无效”。我只用一个转换构造函数测试了类似的版本。这些也不起作用。

问题:

  1. 为什么编译器会尝试调用字符串参数版本?
  2. 为什么bar()中<Foo>的附加值很重要。
  3. 如何防止这种情况发生。例如。我可以强制编译器在输入bool时选择bar( const Foo& )吗?
  4. 或者,如果有人拨打bar< Foo >( false )
  5. ,是否可以强制执行编译错误

3 个答案:

答案 0 :(得分:4)

以下是四个问题的答案:

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

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

    C ++ 2011中提供了删除功能。

    主要问题似乎是:如果Foostd::string都可以从bool转换,那么为什么std::string选择bar<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::string版本之间调用Foo会有歧义。

答案 1 :(得分: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;
}

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

答案 2 :(得分:1)

这一行

bar< Foo >( false );

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

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

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

至少......大概是正在发生的事情,虽然它真的不应该选择促销+建筑而不仅仅是建筑序列。