c++ 为什么变量模板构造函数比复制构造函数匹配得更好?

rggaifut  于 2022-11-19  发布在  其他
关注(0)|答案(4)|浏览(178)

下列程式码不会编译:

#include <iostream>
#include <utility>

struct Foo
{
    Foo() { std::cout << "Foo()" << std::endl; }
    Foo(int) { std::cout << "Foo(int)" << std::endl; }
};

template <typename T>
struct Bar
{
    Foo foo;

    Bar(const Bar&) { std::cout << "Bar(const Bar&)" << std::endl; }

    template <typename... Args>
    Bar(Args&&... args) : foo(std::forward<Args>(args)...)
    {
        std::cout << "Bar(Args&&... args)" << std::endl;
    }
};

int main()
{
    Bar<Foo> bar1{};
    Bar<Foo> bar2{bar1};
}

编译器错误提示我,编译器试图使用可变模板构造函数而不是复制构造函数:

prog.cpp: In instantiation of 'Bar<T>::Bar(Args&& ...) [with Args = {Bar<Foo>&}; T = Foo]':
prog.cpp:27:20:   required from here
prog.cpp:18:55: error: no matching function for call to 'Foo::Foo(Bar<Foo>&)'
  Bar(Args&&... args) : foo(std::forward<Args>(args)...)

为什么编译器会这样做?如何修复?

5lhxktic

5lhxktic1#

这叫:

Bar<Foo> bar2{bar1};

在其多载集中有两个候选:

Bar(const Bar&);
Bar(Bar&);       // Args... = {Bar&}

确定一个转换序列是否比另一个更好的方法之一是,根据[over.ics.rank]:
标准转换序列S1是比标准转换序列S2更好的转换序列,如果

  • [...]
  • S1和S2是指涉系结(8.5.3),而且指涉所指涉的型别除了最上层的 cv-限定词之外,都是相同的型别,而且S2所初始化的指涉所指涉的型别比S1所初始化的指涉所指涉的型别更符合 cv-限定。[范例:
int f(const int &);
int f(int &);
int g(const int &);
int g(int);

int i;
int j = f(i);    // calls f(int &)
int k = g(i);    // ambiguous
  • -结束示例]*

转发引用可变构造函数是一个更好的匹配,因为它的引用绑定(Bar&)比复制构造函数的引用绑定(const Bar&)更少 cv 限定。
至于解决方案,只要Args...是您应该使用SFINAE调用复制或移动构造函数的对象,您就可以将其从候选集中排除:

template <typename... > struct typelist;

template <typename... Args,
          typename = std::enable_if_t<
              !std::is_same<typelist<Bar>,
                            typelist<std::decay_t<Args>...>>::value
          >>
Bar(Args&&... args)

如果Args...BarBar&Bar&&const Bar&中的一个,那么typelist<decay_t<Args>...>就是typelist<Bar>--我们要排除这种情况,而Args...的任何其他集合都是可以的。

rbl8hiat

rbl8hiat2#

虽然我同意这是违反直觉的,但原因是你的复制构造函数接受const Bar&,但bar1不是常量。
http://coliru.stacked-crooked.com/a/2622b4871d6407da
因为通用引用可以绑定任何东西,所以选择它而不是具有const要求的更具限制性的构造函数。

syqv5f0l

syqv5f0l3#

另一种避免选择变元构造函数的方法是提供Bar构造函数的所有形式。
这需要更多的工作,但避免了enable_if的复杂性,如果这对您很重要的话:

#include <iostream>
#include <utility>

struct Foo
{
    Foo() { std::cout << "Foo()" << std::endl; }
    Foo(int) { std::cout << "Foo(int)" << std::endl; }
};

template <typename T>
struct Bar
{
    Foo foo;

    Bar(const Bar&) { std::cout << "Bar(const Bar&)" << std::endl; }
    Bar(Bar&) { std::cout << "Bar(Bar&)" << std::endl; }
    Bar(Bar&&) { std::cout << "Bar(Bar&&)" << std::endl; }

    template <typename... Args>
    Bar(Args&&... args) : foo(std::forward<Args>(args)...)
    {
        std::cout << "Bar(Args&&... args)" << std::endl;
    }
};

int main()
{
    Bar<Foo> bar1{};
    Bar<Foo> bar2{bar1};
}
mhd8tkvw

mhd8tkvw4#

解决这个问题的“std-way”是首先放置一个std::in_place_t参数。这样你就有了一个明确的类型来强制编译器在你想要的时候使用模板化的构造函数,而在你不想要的时候不让它匹配。你可以在这里检查它是如何完成的https://en.cppreference.com/w/cpp/utility/optional/optional

相关问题