【问题标题】:Incorrect overload/specialization due to wrong conversion由于错误转换导致的错误重载/特化
【发布时间】:2012-10-19 14:45:26
【问题描述】:

在我自己的代码中出现错误之后,(在我看来)编译器选择了错误的重载,我一直在寻找解释,但找不到一个简单的解释。我确实找到了处理专业化问题的Herb Sutter's 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 的两个构造函数接受 bool,“使”Foo 的选择“无效”。我只用一个转换构造函数测试了类似的版本。这些也不起作用。

问题:

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

【问题讨论】:

    标签: c++


    【解决方案1】:

    以下是四个问题的答案:

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

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

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

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

    主要问题似乎是:如果Foostd::string 都可以从bool 转换,为什么在调用bar&lt;Foo&gt;(bool) 并且以下重载可用时选择std::string 转换?

    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&amp;) 作为比推导模板参数的版本更专业的接口,所以选择了这个版本。在这个阶段忽略第一个模板的特化。要使Foo 的使用也适用,您可以添加

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

    调用bar&lt;Foo&gt;(false) 会在std::stringFoo 版本之间产生歧义。

    【讨论】:

    • 第一个回答了所有四个问题。谢谢。关于答案 1: Foo 也可以从 false 构造...为什么它选择 bar( string ) 而不是 bar( Foo )?
    • 您在对已删除答案的评论中说,您一开始只使用了 const bool&amp; 构造函数。问题是,我不认为文字 falseconst bool&amp;。您可以尝试评论该构造函数,看看它是否有效?
    • 已经尝试了所有三种组合:两种构造函数,一种使用 bool,另一种使用 const bool&。不工作。
    • 啊,实际上,不:std::string 版本被选择而不是推导模板参数的版本,因为它更专业。采用Foo 的专用版本看起来同样专用,但实际上并非如此,因为首先重载解析选择了最匹配的主模板,然后用专用化替换它(如果适用)。
    • 是的,我想是的:template &lt;typename&gt; void bar(bool);.
    【解决方案2】:

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

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

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

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

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

    【讨论】:

      【解决方案3】:

      这一行

      bar< Foo >( false );
      

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

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

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

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

      【讨论】:

      • 如果它匹配这两种方法,为什么编译器不抱怨“模糊重载”(也作为问题添加)?
      • Dietmar 的回答解释说,拥有 两个 隐式构造函数可以同时匹配相同的优先级,从而将 Foo 构造路径从考虑中移除。你每天都能学到一些东西!
      猜你喜欢
      • 1970-01-01
      • 2018-12-09
      • 1970-01-01
      • 1970-01-01
      • 2018-10-20
      • 1970-01-01
      • 2013-06-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多