使用成员初始值设定项时 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 && ./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&) [with T = std::vector<part>]
通话。
我正在做一个使用 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++
与我使用的简单虚拟 struct
s 的行为不同(这就是为什么我不能用更简单的结构编写 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::part
是 public
),我有以下内容:
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::part
的vector
,它是(错误地)用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::string
的begin
和end
迭代器构造时得到zmq::message::part::part(Iter, Iter) [with Iter = __gnu_cxx::__normal_iterator<char*, std::__cxx11::basic_string<char> >]
。
@Arda - 我建议查看part
的构造函数和运算符,因为它们在您更改的构造函数中使用。查看message
中的那些函数不会有什么坏处,但是(根据您的描述)您更改的message
的构造函数不使用message
的移动构造函数或其他操作。
您可以进一步简化。完全删除message
并将其放入main
:std::vector<part> vp1; std::vector<part> vp2std::move(vp1);
。并且您可以摆脱part
中的成员,从而摆脱警告。
【参考方案1】:
Clang 实现了 DR 1467(大括号从 T
初始化 T
的行为就像您没有使用大括号一样)但还没有实现 DR 2137(再想一想,只对聚合执行此操作) .
您的part
可以隐式转换为阳光下的所有内容,包括std::vector<part>
。由于std::vector<part>
不是聚合,所以在DR2137 之后,通常的两阶段重载解决方案发生在parts_std::move(parts)
并选择initializer_list<part>
构造函数,将std::move(parts)
转换为part
。
【讨论】:
我会阅读更多关于此的内容。非常感谢您的回答。基本上,由于嵌套初始化列表中的括号省略,我是否遇到了这个问题?您能否详细说明我的问题的可能解决方案?例如,模板化构造函数上的 SFINAE 阻止了除算术类型之外的任何东西的构造似乎已经奏效。我应该坚持那个吗? 独角兽初始化的规则基本上是“如果参数可以以某种方式扭曲为匹配初始化列表构造函数,则使用该构造函数”。一种解决方法是使用括号而不是大括号:parts_(std::move(parts))
。然后是 part
可以从任何事物隐式转换,这似乎很值得怀疑,尽管我对库的了解还不够确定。根据预期用途,SFINAE 约束构造函数和/或使其显式化都是可能的选项。
我不想在我的用例中进行显式构造;但绝对将其限制为我想要的类型是最好的。我也会按照建议更换牙套。非常感谢您的帮助!我已经选择了这个作为答案。但是,显然,我还需要 18 个小时来奖励赏金。干杯!以上是关于使用成员初始值设定项时 gcc 中可能存在的错误的主要内容,如果未能解决你的问题,请参考以下文章
不能在属性初始值设定项中使用实例成员 '';属性初始化程序在 'self' 可用之前运行