使用成员初始值设定项时 gcc 中可能存在的错误

Posted

技术标签:

【中文标题】使用成员初始值设定项时 gcc 中可能存在的错误【英文标题】:Possible bug in gcc when using member initializer 【发布时间】:2018-04-13 13:09:29 【问题描述】:

编辑。最后,我创建了关于下面原始问题的最小工作示例(感兴趣的读者可以阅读下面的长篇文章)。

基本上,g++clang++ 对以下代码摘录的解释不同,这导致了头痛:

#include <iostream>
#include <vector>

struct part 
  part() = default;
  template <class T> part(const T &) 
    std::cout << __PRETTY_FUNCTION__ << '\n';
  
;

struct message 
  message() = default;
  message(std::vector<part> parts) : parts_std::move(parts) 

  std::size_t size() const noexcept  return parts_.size(); 

private:
  std::vector<part> parts_;
;

int main(int argc, char *argv[]) 
  part p15, p26.8;

  message msg = p1, p2;

  std::cout << "msg.size(): " << msg.size() << '\n';

  return 0;

当我使用clang++ -Wall -std=c++11 -O3 mwe.cpp -o mwe.out &amp;&amp; ./mwe.out 编译上述代码时,我得到以下信息:

part::part(const T &) [T = int]
part::part(const T &) [T = double]
msg.size(): 2 

当使用g++ 编译相同的代码时,我得到以下信息:

part::part(const T&) [with T = int]
part::part(const T&) [with T = double]
part::part(const T&) [with T = std::vector<part>]
msg.size(): 1

不过,我不希望看到最后一个 part::part(const T&amp;) [with T = std::vector&lt;part&gt;] 通话。


我正在做一个使用 0MQ 的项目,因此使用了 zeromq 标签。

我在我的代码中遇到了一个奇怪的问题,我不确定这是 g++ 中的错误还是我包装 0MQ 库中的错误。我希望我能从你那里得到一些帮助。基本上,我正在测试

~> g++ --version
g++ (GCC) 7.3.1 20180312
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

~> clang++ --version
clang version 6.0.0 (tags/RELEASE_600/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

您可以在我的 GitHub 帐户中找到 zmq.hpp 文件,由于它太长,我不想在这里粘贴它。我基于该标题的最小工作示例将显示为:

#include <iostream>
#include <string>

#include "zmq.hpp"

int main(int argc, char *argv[]) 
  auto version = zmq::version();
  std::cout << "0MQ version: v" << std::get<0>(version) << '.'
            << std::get<1>(version) << '.' << std::get<2>(version) << '\n';

  zmq::message msg1, msg2;
  std::string p1"part 1";
  uint16_t p25;
  msg1.addpart(std::begin(p1), std::end(p1));
  msg1.addpart(p2);

  std::cout << "msg1 is a " << msg1.numparts() << "-part message.\n";
  std::cout << "msg1[0]: " << static_cast<char *>(msg1.data(0)) << '\n';
  std::cout << "msg1[1]: " << *static_cast<uint16_t *>(msg1.data(1)) << '\n';

  msg2 = msg1[0], msg1[1];

  std::cout << "msg2 is a " << msg2.numparts() << "-part message.\n";
  std::cout << "msg2[0]: " << static_cast<char *>(msg2.data(0)) << '\n';
  std::cout << "msg2[1]: " << *static_cast<uint16_t *>(msg2.data(1)) << '\n';

  return 0;

当我编译代码时

~> clang++ -Wall -std=c++11 -O3 mwe.cpp -o mwe.out -lzmq
~> ./mwe.out

我看到以下输出:

0MQ version: v4.2.5
msg1 is a 2-part message.
msg1[0]: part 1
msg1[1]: 5
msg2 is a 2-part message.
msg2[0]: part 1
msg2[1]: 5

但是,当我用

编译代码时
~> g++ -Wall -std=c++11 -O3 mwe.cpp -o mwe.out -lzmq
~> ./mwe.out

我得到以下信息:

0MQ version: v4.2.5
msg1 is a 2-part message.
msg1[0]: part 1
msg1[1]: 5
msg2 is a 1-part message.
msg2[0]: <some garbage here>
fish: “./mwe.out” terminated by signal SIGSEGV (Address boundary error)

显然,由于我读取了一个我不拥有的内存位置,我收到了SIGSEGV。有趣的是,当我将zmq.hpp 文件的Line 764 更改为读取时:

// message::message(std::vector<part> parts) noexcept : parts_std::move(parts) 
message::message(std::vector<part> parts) noexcept 
  parts_ = std::move(parts);

当使用这两种编译器编译时,代码会按预期工作。

简而言之,我想知道我是否在做一些可疑的事情导致g++ 编译的代码不起作用,或者g++ 有可能存在一些错误。 g++ 与我使用的简单虚拟 structs 的行为不同(这就是为什么我不能用更简单的结构编写 MWE,这就是我怀疑我的包装器的原因)。而且,-O0 -g 开关也观察到相同的行为。

提前感谢您的宝贵时间。

编辑。我已将 MWE 更改为如下所示(根据 @Peter 的评论):

#include <iostream>
#include <string>

#include "zmq.hpp"

int main(int argc, char *argv[]) 
  auto version = zmq::version();
  std::cout << "0MQ version: v" << std::get<0>(version) << '.'
            << std::get<1>(version) << '.' << std::get<2>(version) << '\n';

  zmq::message msg1, msg2;
  std::string data1"part 1";
  uint16_t data25;
  msg1.addpart(std::begin(data1), std::end(data1));
  msg1.addpart(data2);

  std::cout << "msg1 is a " << msg1.numparts() << "-part message.\n";
  // std::cout << "msg1[0]: " << static_cast<char *>(msg1.data(0)) << '\n';
  // std::cout << "msg1[1]: " << *static_cast<uint16_t *>(msg1.data(1)) << '\n';

  msg2 = msg1[0], msg1[1];

  std::cout << "msg2 is a " << msg2.numparts() << "-part message.\n";
  // std::cout << "msg2[0]: " << static_cast<char *>(msg2.data(0)) << '\n';
  // std::cout << "msg2[1]: " << *static_cast<uint16_t *>(msg2.data(1)) << '\n';

  zmq::message::part p1 = 5.0; // double
  std::cout << "[Before]: p1 has size " << p1.size() << '\n';
  zmq::message::part p2std::move(p1);
  std::cout << "[After]: p1 has size " << p1.size() << '\n';
  std::cout << "[After]: p2 has size " << p2.size() << '\n';

  zmq::message::part p3;
  std::cout << "[Before]: p3 has size " << p3.size() << '\n';
  p3 = std::move(p2);
  std::cout << "[After]: p2 has size " << p2.size() << '\n';
  std::cout << "[After]: p3 has size " << p3.size() << '\n';

  return 0;

使用 g++ 和我在 GitHub gist 中提供的原始 zmq.hpp 文件(嗯,这次 message::partpublic),我有以下内容:

0MQ version: v4.2.5
msg1 is a 2-part message.
msg2 is a 1-part message.
[Before]: p1 has size 8
[After]: p1 has size 0
[After]: p2 has size 8
[Before]: p3 has size 0
[After]: p2 has size 0
[After]: p3 has size 8

但是,当我使用 clang++ 时,我得到以下信息:

0MQ version: v4.2.5
msg1 is a 2-part message.
msg2 is a 2-part message.
[Before]: p1 has size 8
[After]: p1 has size 0
[After]: p2 has size 8
[Before]: p3 has size 0
[After]: p2 has size 0
[After]: p3 has size 8

移动构造和移动分配似乎都适用于message::part 对象。最后,valgrind ./mwe.out 没有泄漏或错误。

编辑。我已经在周末调试了代码。看来g++正在打电话

template <class T> message::part::part(const T &value) : part(sizeof(T)) 
  std::memcpy(zmq_msg_data(&msg_), &value, sizeof(T));

std::move 之后

message::message(std::vector<part> parts) noexcept : parts_std::move(parts) 

因此,它创建了一个只有 1 个message::partvector,它是(错误地)用value = msg1[0], msg1[1] 构造的。但是,clang++ 做了正确的事情,没有调用模板化的构造函数。

有没有办法解决这个问题?

编辑。我已经相应地修改了代码:

struct message 
private:
  struct part 
    /* ... */
    part(const T &,
         typename std::enable_if<std::is_arithmetic<T>::value>::type * =
             nullptr);
    /* ... */
  ;
    /* ... */
;

template <class T>
message::part::part(
    const T &value,
    typename std::enable_if<std::is_arithmetic<T>::value>::type *)
    : part(sizeof(T)) 
  std::memcpy(zmq_msg_data(&msg_), &value, sizeof(T));

现在,g++clang++ 编译的二进制文件都可以正常工作。显然,模板化构造函数上的 SFINAE 禁用了之前调用过的 initializer_list 上的构造函数调用,问题得到解决。

但是,我仍然想知道为什么g++ 更喜欢模板化构造函数调用而不是clang++ 选择的正常移动操作。

【问题讨论】:

我没有查看您的标题,但根据您的描述,原因可能是 part 的移动构造函数。如果您描述的更改缓解了症状,请查找(1)默认构造然后移动分配(有效)和(2)移动构造之间的区别。我不会打赌它是一个编译器错误 - 未定义行为的一个“乐趣”是它似乎可以在一个编译器上正确运行,但在另一个编译器上运行失败。 您似乎将std::string 添加到zmq::message,但随后将zmq::message::data 的返回值从void * 转换为char *。您的标头代码似乎也使用std::memcpy 来复制非平凡类型。 @G.M.,感谢您的提醒。你认为我应该用什么来代替std::memcpy?我应该到处使用std::copy,还是应该使用std::enable_if,例如,只允许复制琐碎的类型?不过,我怀疑这是这里的问题,因为我没有调用该构造函数。我使用__PRETTY_FUNCTION__ 来检查迭代器,从std::stringbeginend 迭代器构造时得到zmq::message::part::part(Iter, Iter) [with Iter = __gnu_cxx::__normal_iterator&lt;char*, std::__cxx11::basic_string&lt;char&gt; &gt;] @Arda - 我建议查看part 的构造函数和运算符,因为它们在您更改的构造函数中使用。查看message 中的那些函数不会有什么坏处,但是(根据您的描述)您更改的message 的构造函数不使用message 的移动构造函数或其他操作。 您可以进一步简化。完全删除message 并将其放入mainstd::vector&lt;part&gt; vp1; std::vector&lt;part&gt; vp2std::move(vp1);。并且您可以摆脱part 中的成员,从而摆脱警告。 【参考方案1】:

Clang 实现了 DR 1467(大括号从 T 初始化 T 的行为就像您没有使用大括号一样)但还没有实现 DR 2137(再想一想,只对聚合执行此操作) .

您的part 可以隐式转换为阳光下的所有内容,包括std::vector&lt;part&gt;。由于std::vector&lt;part&gt; 不是聚合,所以在DR2137 之后,通常的两阶段重载解决方案发生在parts_std::move(parts) 并选择initializer_list&lt;part&gt; 构造函数,将std::move(parts) 转换为part

【讨论】:

我会阅读更多关于此的内容。非常感谢您的回答。基本上,由于嵌套初始化列表中的括号省略,我是否遇到了这个问题?您能否详细说明我的问题的可能解决方案?例如,模板化构造函数上的 SFINAE 阻止了除算术类型之外的任何东西的构造似乎已经奏效。我应该坚持那个吗? 独角兽初始化的规则基本上是“如果参数可以以某种方式扭曲为匹配初始化列表构造函数,则使用该构造函数”。一种解决方法是使用括号而不是大括号:parts_(std::move(parts))。然后是 part 可以从任何事物隐式转换,这似乎很值得怀疑,尽管我对库的了解还不够确定。根据预期用途,SFINAE 约束构造函数和/或使其显式化都是可能的选项。 我不想在我的用例中进行显式构造;但绝对将其限制为我想要的类型是最好的。我也会按照建议更换牙套。非常感谢您的帮助!我已经选择了这个作为答案。但是,显然,我还需要 18 个小时来奖励赏金。干杯!

以上是关于使用成员初始值设定项时 gcc 中可能存在的错误的主要内容,如果未能解决你的问题,请参考以下文章

让编译器检查数组初始值设定项的数量

C++ 不能用初始值设定项列表初始化非聚合错误

不能在属性初始值设定项中使用实例成员 '';属性初始化程序在 'self' 可用之前运行

“Emgu.CV.CvInvoke”的类型初始值设定项引发异常。

成员初始值设定项不命名非静态数据成员

java中局部变量和成员变量主要是他们作用域的区别