从括号内的initializer_list构造时调用错误的重载

cra*_*yze 6 c++ visual-c++ language-lawyer c++11 clang++

我一直以为当我使用初始化列表C++语法时:

something({ ... });
Run Code Online (Sandbox Code Playgroud)

编译器总是清楚我想要调用过载std::initializer_list,但对于MSVC 2015来说似乎并不那么清楚.

我测试了这个简单的代码:

#include <cstdio>
#include <initializer_list>

namespace testing {
  template<typename T>
  struct Test {
    Test() {
      printf("Test::Test()\n");
    }

    explicit Test(size_t count) {
      printf("Test::Test(int)\n");
    }

    Test(std::initializer_list<T> init) {
      printf("Test::Test(std::initializer_list)\n");
    }

    T* member;
  };

  struct IntSimilar {
    int val;

    IntSimilar() : val(0) {}
    IntSimilar(int v) : val(v) {}

    operator int() {
      return val;
    }
  };
}

int main() {
    testing::Test<testing::IntSimilar> obj({ 10 });
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

并且在GCC 6.3中它按预期工作,呼叫 Test::Test(std::initializer_list)

但在MSVC 2015中,此代码调用Test::Test(int).

似乎MSVC可以以某种方式忽略{}并选择无效/意外的重载来调用.

标准对这种情况有何看法?哪个版本有效?

任何人都可以对此进行测试并确认此问题是否仍然存在于MSVC 2017中?

Jod*_*cus 5

哪个版本有效?

根据我对标准的理解,海湾合作委员会是对的.

标准对这种情况有何看法?

您在编写时所执行的操作Test obj1({10});是使用表达式直接初始化类型的对象.在重载解析期间,编译器必须决定调用哪个构造函数.根据16.3.3.2§3(3.1.1)[over.ics.rank]:Test{ 10 }

列表初始化序列L1是比列表初始化序列更好的转换序列,L2如果L1转换 std::initializer_list<X>为某些X并且L2不[...]

该标准还提供了示例

void f1(int);                                 // #1
void f1(std::initializer_list<long>);         // #2
void g1() { f1({42}); }                       // chooses #2
Run Code Online (Sandbox Code Playgroud)

这是VS&clang与GCC不同的地方:虽然这三个在这个特定的例子中会产生相同的结果,但是将它改为

#include <iostream>

struct A { A(int) { } };
void f1(int) { std::cout << "int\n"; }                                // #1
void f1(std::initializer_list<A>) { std::cout << "list\n"; }          // #2

int main() {
    f1({42});
}
Run Code Online (Sandbox Code Playgroud)

会让clang选择int-constructor,对文字周围不必要的括号抱怨42(这似乎只是标准遗留原因,请参见此处),而不是检查{ 42 }列表序列是否真的无法转换为std::initializer_list<A>.

但请注意,写入Test obj1{ 10 };将导致不同的评估:根据列表初始化的规则:

  • 否则,T的构造函数分为两个阶段:
    • 将std :: initializer_list作为唯一参数的所有构造函数,或者作为第一个参数,如果其余参数具有默认值,将检查所有构造函数,并通过重载决策与std :: initializer_list类型的单个参数进行匹配

因此initializer_list构造函数用于特殊的重载解析阶段initializer_list,在应用正常的重载分辨率之前仅考虑构造函数,如着名的std::vector-gotcha所示:

// will be a vector with elements 2, 0 rather than a vector of size 2 with values 0, 0
std::vector<int> v{ 2, 0 };
Run Code Online (Sandbox Code Playgroud)

事实上,在两种情况下,标准决定使用initializer_list构造函数是一致的选择,但从技术上讲,选择它的原因在引擎盖下是完全不同的.


rus*_*tyx 2

GCC在这里错了

事实上,由于括号是直接初始化,因此适用“正常”重载规则,但是,[over.ics.rank]/3.1讨论了这种情况:

void f1(int);                                 // #1
void f1(std::initializer_list<long>);         // #2
void g1() { f1({42}); }                       // chooses #2
Run Code Online (Sandbox Code Playgroud)

而在我们的情况下,我们有这样的情况:

struct IntSimilar { IntSimilar(int); };

void f1(size_t);                              // #1
void f1(std::initializer_list<IntSimilar>);   // #2
void g1() { f1({10}); }                       // chooses ?
Run Code Online (Sandbox Code Playgroud)

还有另一条规则,[over.ics.rank]/2就在 [over.ics.rank]/3 之前:

— 标准转换序列是比用户定义的转换更好的转换序列

为了调用Test(initializer_list<IntSimilar>)用户定义的转换,需要 ( intto IntSimilar)。但有一个更好的可行替代方案,特别是从到 的整数转换。这是可能的,因为标量(例如 )可以从带有单个元素的花括号初始化列表进行列表初始化。请参阅[dcl.init.list]/3.9intsize_tintint

— 否则,如果初始值设定项列表具有类型 E 的单个元素,并且 T 不是引用类型或其引用类型与 E 引用相关,则从该元素初始化对象或引用...

clang 实际上会准确地告诉你这一点(在选择重载时int):

void f1(int);                                 // #1
void f1(std::initializer_list<long>);         // #2
void g1() { f1({42}); }                       // chooses #2
Run Code Online (Sandbox Code Playgroud)

如果您想禁止自动展开单值braced-init-list,请使用list-initialization或将其包装到另一个braced-init-list中:

    testing::Test<testing::IntSimilar> obj { 10 };
    testing::Test<testing::IntSimilar> obj({{10}});
Run Code Online (Sandbox Code Playgroud)

- 将在各处选择initializer_list<T>过载。