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

时间:2017-12-02 13:28:08

标签: c++ c++11 visual-c++ language-lawyer clang++

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

something({ ... });

编译器总是清楚我想调用带有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

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

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

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

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

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

2 个答案:

答案 0 :(得分:5)

  

哪个版本有效?

根据我对该标准的理解, GCC 正确

  

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

您在撰写Test obj1({10});时所执行的操作direct-initializingTest类型的对象,其表达式为{ 10 }。在重载解析期间,编译器必须决定调用哪个构造函数。根据{{​​3}}:

  

列表初始化序列L1是一个比列表初始化序列L2更好的转换序列,如果L1转换为   某些std::initializer_list<X>X的{​​{1}}不会[...]

该标准还提供了示例

L2

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

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

会让16.3.3.2 § 3 (3.1.1) [over.ics.rank]抱怨文字#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}); } 周围不必要的大括号(由于遗留原因似乎只是标准,请参阅clang chose the int-constructor),而不是检查{ {1}}列表序列实际上无法转换为42

但请注意,撰写{ 42 }会导致不同的评价:根据here的规则:

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

所以std::initializer_list<A>构造函数用于特殊的重载决策阶段,在应用正常的重载决策之前只考虑Test obj1{ 10 };构造函数,如着名的initializer_list - gotcha:

initializer_list

事实上,在这两种情况下,标准决定使用std::vector构造函数是一个一致的选择,但从技术上讲,选择它的原因是完全不同的。

答案 1 :(得分:0)

GCC在这里错了

实际上由于括号,它是直接初始化所以“正常”的重载规则适用,但是,[over.ics.rank]/3.1谈论这种情况:

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

我们的情况下,我们有:

struct IntSimilar { IntSimilar(int); };

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

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

  

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

要调用Test(initializer_list<IntSimilar>),需要用户定义的转换intIntSimilar)。 但是有一个更好的可行替代方案,特别是从intsize_t整数转换。这是可能的,因为标量(例如int)可以使用单个int元素从 braced-init-list 进行列表初始化。见[dcl.init.list]/3.9

  

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

clang实际上会告诉你(在选择int重载时):

    warning: braces around scalar initializer [-Wbraced-scalar-init]

如果要禁止单值 braced-init-list 的自动解包,请使用 list-initialization 或将其包装到另一个 braced- INIT-列表

    testing::Test<testing::IntSimilar> obj { 10 };
    testing::Test<testing::IntSimilar> obj({{10}});

- 将在任何地方选择initializer_list<T>重载。