使用成员初始值设定项时 gcc 中可能存在的错误
Possible bug in gcc when using member initializer
编辑。 最后,我创建了关于下面原始问题的最小工作示例(感兴趣的 reader 可以阅读较长的 post接下来)。
基本上,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 p1{5}, p2{6.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
文件,我不想将其粘贴到这里,因为它太长了。我基于 header 的最小工作示例将是:
#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 p2{5};
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
没有相同的行为(这就是为什么我不能用更简单的结构编写 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 data2{5};
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 p2{std::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 要点中提供的原始 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
objects。最后,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++
选择的正常移动操作。
Clang 实现了 DR 1467 (brace-initializing a T
from a T
behaves as if you didn't use braces) but has yet to implement DR 2137(转念一想,只对聚合执行此操作)。
你的 part
可以从阳光下的一切隐式转换,包括 std::vector<part>
。由于 std::vector<part>
不是聚合,post-DR2137 通常的两阶段重载决议发生在 parts_{std::move(parts)}
并选择 initializer_list<part>
构造函数,转换 std::move(parts)
进入 part
.
编辑。 最后,我创建了关于下面原始问题的最小工作示例(感兴趣的 reader 可以阅读较长的 post接下来)。
基本上,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 p1{5}, p2{6.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
文件,我不想将其粘贴到这里,因为它太长了。我基于 header 的最小工作示例将是:
#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 p2{5};
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
没有相同的行为(这就是为什么我不能用更简单的结构编写 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 data2{5};
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 p2{std::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 要点中提供的原始 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
objects。最后,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++
选择的正常移动操作。
Clang 实现了 DR 1467 (brace-initializing a T
from a T
behaves as if you didn't use braces) but has yet to implement DR 2137(转念一想,只对聚合执行此操作)。
你的 part
可以从阳光下的一切隐式转换,包括 std::vector<part>
。由于 std::vector<part>
不是聚合,post-DR2137 通常的两阶段重载决议发生在 parts_{std::move(parts)}
并选择 initializer_list<part>
构造函数,转换 std::move(parts)
进入 part
.