使用成员初始值设定项时 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::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 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::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++ 选择的正常移动操作。

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.