类层次结构中完美转发构造函数和复制构造函数之间的冲突

7
我最近在尝试使用完美转发构造函数实现类层次结构时遇到了一个问题。 考虑以下示例:
struct TestBase {
  template<typename T>
  explicit TestBase(T&& t) : s(std::forward<T>(t)) {} // Compiler refers to this line in the error message

  TestBase(const TestBase& other) : s(other.s) {}

  std::string s;
};

struct Test : public TestBase {
  template<typename T>
  explicit Test(T&& t) : TestBase(std::forward<T>(t)) {}

  Test(const Test& other) : TestBase(other) {}
};

当我尝试编译代码时,我得到了以下错误:
错误3 error C2664: 'std::basic_string<_Elem,_Traits,_Alloc>::basic_string(const std::basic_string<_Elem,_Traits,_Alloc> &)' :无法将参数1从'const Test'转换为'const std::basic_string<_Elem,_Traits,_Alloc> &'
我的理解是,编译器将完美转发构造函数视为比复制构造函数更好的匹配。例如,请参阅Scott Meyers:Copying Constructors in C++11。在没有类层次结构的其他实现中,我可以通过SFINAE禁用完美转发构造函数作为复制构造函数。请参阅Martinho Fernandes:Some pitfalls with forwarding constructors。当我尝试将上述解决方案应用于此示例时,仍然无法使用相同的错误消息进行编译。
我认为一个可能的解决方案是避免完美转发,在构造函数中按值获取参数,然后从它们移动到类变量。
所以我的问题是,如果有其他解决方案来解决这个问题,或者在这种情况下无法进行完美转发?更新:事实证明,我的问题很容易被误解。因此,我将尝试澄清我的意图和背景。代码像问题中发布的那样完整。没有创建其他对象或调用其他函数。在尝试编译发布的示例时出现了错误。拥有完美转发构造函数的目的是为了成员初始化,而不是拥有某种额外的复制构造函数。原因在于当使用临时对象初始化成员时(由Scott Meyers提出),可以节省一些对象副本。不幸的是,正如完美转发构造函数可能与其他重载构造函数(在此示例中为复制构造函数)发生冲突的事实所表明的那样。像这个问题的答案和评论建议的那样:可能的解决方案是引入显式转换或具有单独的非模板构造函数(即关于参数的两个构造函数,一个是const string&,另一个是string&&)。

可能是您写的代码类似于std :: string(test)而不是std :: string(test.s)吗?请向我们展示main.cc文件中的第130行。 - Andriy
2
你写成了s(std::forward<T>(t))而不是s(std::forward<T>(t).s) - avakar
1
通常混合使用重载和转发模板是一个不好的主意。我会从 string 创建一个单一的构造函数,并添加一个转发的 make_test 函数 模板。 - Kerrek SB
@KerrekSB 考虑Test是一个类。在这种情况下,您将如何处理构造函数中的参数赋值,同时避免不必要的副本。您是否建议按值获取字符串并从中移动到成员? - mkh
@mkh:我认为你有些混淆了。你想要转发当你调用一个构造函数时,而不是在定义一个构造函数时。 - Kerrek SB
显示剩余14条评论
3个回答

2

尝试将Test(const Test& other) : TestBase(other) {}更改为Test(const Test& other) : TestBase(static_cast<TestBase const&>(other)) {}

第二个Test构造函数调用了TestBase,有两种可能性。其中一种接受任何参数,另一种接受TestBase。但是您正在向它传递一个Test——“任何”更匹配。通过显式转换为TestBase const&,我们应该能够获得正确匹配的结果。

另一种可能性可能涉及如何构建Test——也许您传递的内容匹配了Test的模板构造函数?我们可以通过从Test中删除模板构造函数并查看错误是否消失来测试这种其他可能性。

如果是这种情况,为什么您链接的技术(当类型推断匹配Test时禁用Test模板构造函数)不能起作用?


你的解决方案编译没有错误。在应用你的解决方案时,可能会有任何可能的副作用吗?通常情况下,在使用转换器来消除错误/警告时,我喜欢小心一点。 - mkh
涉及的转换是安全的。我将Foo const&转换为FooBase const&,这是你想要隐式执行的操作,但编译器更喜欢接受一切的模板构造函数。我明确了你想要隐含发生的事情... - Yakk - Adam Nevraumont
谢谢您的解释。我还有一个问题:如果我现在像这样为Test添加一个移动构造函数:Test(Test&& other) : TestBase(std::move(other)) {},我会再次收到相同的错误消息。您是否可能也有解决此情况的方法?非常感谢! - mkh
+1,这是一个非常微妙的事情。我盯着显示器看了一会儿才意识到发生了什么。 - avakar
@Yakk 好的,我想我懂你的意思了。但是考虑到我也像这样在TestBase中添加了一个移动构造函数:TestBase(TestBase&& other) : s(std::move(other.s)) {} 如果我理解正确,那应该比完美转发构造函数更匹配,对吗?但是仍然出现相同的错误。抱歉有点固执——只是想弄清楚而不会让自己更加困惑。 - mkh
显示剩余2条评论

1

以下代码应该可以正常工作,且不使用显式转换:

struct Test : public TestBase {
  private: 
  static TestBase const& toBase(const Test& o) { return o; }

  public:
  template <typename T>
  explicit Test(T&& t) : TestBase(std::forward<T>(t)) {}

  Test(const Test& other) : TestBase(toBase(other)) {} 
};

1

让我们仔细看一下错误信息。

std::basic_string<...>::basic_string(const std::basic_string<...> &) :

这意味着它适用于std::string的复制构造函数。

cannot convert parameter 1 from 'const Test' to 'const std::basic_string<..> &

确实,无法从Test转换为std::string。但是,Test有一个字符串成员,即std::string s;

结论:看起来您忘记在那个位置添加.s了。可能是在s(std::forward<T>(t))中。

另一个可能的原因是,在复制构造Test实例时选择了构造函数的第一个重载而不是第二个重载。


我不认为我忘记了 .s。我的想法是参数 t 本身就是某种字符串,我想将其转发给成员。例如 const char* 或 std::string。 - mkh
@mkh:那么这意味着T不是一个字符串,而是Test - Andriy
那么我的问题是,我该如何让编译器停止认为 T 可能是一个 Test。我没有创建任何对象或调用任何函数。 - mkh
@Andrey жҳҜзҡ„ - зңӢиө·жқҘ Test дёӯзҡ„жһ„йҖ еҮҪж•°2ж— ж„Ҹдёӯи°ғз”ЁдәҶTestBaseдёӯзҡ„жһ„йҖ еҮҪж•°1гҖӮ然еҗҺ他们иҜ•еӣҫе°ҶдёҖдёӘTestиҪ¬жҚўдёәеӯ—з¬ҰдёІпјҢдҪҶеӨұиҙҘдәҶгҖӮ - Yakk - Adam Nevraumont

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