如何避免在调试宏中移动构造函数

How to avoid the move constructor in debug macros?

本文关键字:移动 构造函数 调试 何避免      更新时间:2023-10-16

我正在尝试编写一个调试宏/模板,该宏/模板输出函数名称,后跟返回值,然后返回该值。我提出了4个makros:RETURN用于基本类型,RETURN_P用于std包装器中的指针(uniqe_ptr,shared_ptr,可选),RETURN_B用于布尔值,RETURN_A用于我只能显示地址的东西。

为了测试宏,我编写了一个Resource类,它是可移动的,但不可复制,并且有一个"私有"构造函数。我使用私有访问参数来防止任何人调用构造函数,同时保留使用std::make_unique等转发函数的能力。这是一个测试对象,所以它为我迄今为止遇到的所有类型提供了工厂。对于每种类型,都有一个工厂作为makro调用的一部分创建该类型,还有一个工厂首先创建一个临时变量,然后用它调用makro

最后,main()函数创建了许多Resource对象来测试所有的makros,包括int和bool基本类型。

现在是我的问题:

  1. 使用定义的NDEBUG,alloc_opt2create2方法调用Resource的move构造函数。这在某种程度上可以避免吗
  2. 当未定义NDEBUG时,alloc_optcreate方法也会调用Resource的move构造函数。我真的很想至少取消这些动作,这样无论有没有NDEBUG,行为都是一样的

有什么可以做的吗?

#include <memory>
#include <tuple>
#include <experimental/optional>
#include <cassert>
#include <stdio.h>
class Resource {
struct access { };
public:
static Resource * alloc_ptr();
static Resource * alloc_ptr2();
static std::unique_ptr<Resource> alloc_uniqe();
static std::unique_ptr<Resource> alloc_uniqe2();
static std::shared_ptr<Resource> alloc_shared();
static std::shared_ptr<Resource> alloc_shared2();
static std::experimental::optional<Resource> alloc_opt();
static std::experimental::optional<Resource> alloc_opt2();
static Resource create();
static Resource create2();
Resource(access);
~Resource();
Resource(Resource &&other);
operator bool() const;
private:
Resource(const Resource &) = delete;
Resource & operator =(const Resource &) = delete;
bool valid;
};
#define NDEBUG
#ifdef NDEBUG
#define RETURN(format, arg) return arg;
#define RETURN_P(arg) return arg;
#define RETURN_B(arg) return arg;
#define RETURN_A(arg) return arg;
#else
template <class T>
T debug(const char *format, const char *name, T && t) {
fprintf(stderr, "[<T>] ");
fprintf(stderr, format, name, t);
return std::forward<T>(t);
}
template <class T>
T debug_p(const char *format, const char *name, T && t) {
fprintf(stderr, "[ptr<T>] ");
fprintf(stderr, format, name, t ? &*t : nullptr);
return std::forward<T>(t);
}
bool debug_b(const char *format, const char *name, bool t) {
fprintf(stderr, "[bool] ");
fprintf(stderr, format, name, t ? "true" : "false");
return t;
}
template <class T>
T debug_a(const char *format, const char *name, T && t) {
fprintf(stderr, "[&<T>] ");
fprintf(stderr, format, name, &t);
return std::forward<T>(t);
}
#define RETURN(format, arg) return debug("%s : " format "n", __PRETTY_FUNCTION__, arg);
#define RETURN_P(arg) return debug_p("%s = %pn", __PRETTY_FUNCTION__, arg);
#define RETURN_B(arg) return debug_b("%s = %sn", __PRETTY_FUNCTION__, arg);
#define RETURN_A(arg) return debug_a("%s = %pn", __PRETTY_FUNCTION__, arg);
#endif
Resource * Resource::alloc_ptr() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
RETURN_P(new Resource(access{}));
}
Resource * Resource::alloc_ptr2() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
Resource * r = new Resource(access{});
RETURN_P(r);
}
std::unique_ptr<Resource> Resource::alloc_uniqe() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
RETURN_P(std::make_unique<Resource>(access{}));
}
std::unique_ptr<Resource> Resource::alloc_uniqe2() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
std::unique_ptr<Resource> r = std::make_unique<Resource>(access{});
RETURN_P(std::move(r));
}
std::shared_ptr<Resource> Resource::alloc_shared() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
RETURN_P(std::make_shared<Resource>(access{}));
}
std::shared_ptr<Resource> Resource::alloc_shared2() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
std::shared_ptr<Resource>  r = std::make_shared<Resource>(access{});
RETURN_P(r);
}
std::experimental::optional<Resource> Resource::alloc_opt() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
RETURN_P(std::experimental::optional<Resource>(std::experimental::in_place, access{}));
}
std::experimental::optional<Resource> Resource::alloc_opt2() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
std::experimental::optional<Resource>  r = std::experimental::optional<Resource>(std::experimental::in_place, access{});
RETURN_P(std::move(r));
}
Resource Resource::create() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
RETURN_A(Resource(access{}));
}
Resource Resource::create2() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
Resource r = Resource(access{});
RETURN_A(std::move(r));
}
Resource::Resource(access) : valid(true) {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
}
Resource::~Resource() {
fprintf(stderr, "%s [%s]n", __PRETTY_FUNCTION__,
valid ? "valid" : "invalid");
valid = false;
}
Resource::Resource(Resource &&other) : valid(other.valid) {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
assert(other.valid);
other.valid = false;
}
Resource::operator bool() const {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
RETURN_B(valid);
}
int test_int() {
fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
RETURN("%d", 1);
}
int main() {
fprintf(stderr, "### test test_int()n");
{
int i = test_int();
fprintf(stderr, "### test test_int() createdn");
if (i) { }
}
fprintf(stderr, "### test test_int() donenn");
fprintf(stderr, "### test Resource::alloc_ptr()n");
{
Resource * r1 = Resource::alloc_ptr();
fprintf(stderr, "### test Resource::alloc_ptr() allocatedn");
delete r1;
}
fprintf(stderr, "### test Resource::alloc_ptr() donenn");
fprintf(stderr, "### test Resource::alloc_ptr2()n");
{
Resource * r1 = Resource::alloc_ptr2();
fprintf(stderr, "### test Resource::alloc_ptr2() allocatedn");
delete r1;
}
fprintf(stderr, "### test Resource::alloc_ptr2() donenn");
fprintf(stderr, "### test Resource::alloc_unique()n");
{
std::unique_ptr<Resource> r2 = Resource::alloc_uniqe();
fprintf(stderr, "### test Resource::alloc_uniqe() allocatedn");
if (r2) { }
}
fprintf(stderr, "### test Resource::alloc_uniqe() donenn");
fprintf(stderr, "### test Resource::alloc_unique2()n");
{
std::unique_ptr<Resource> r2 = Resource::alloc_uniqe2();
fprintf(stderr, "### test Resource::alloc_uniqe2() allocatedn");
if (r2) { }
}
fprintf(stderr, "### test Resource::alloc_uniqe2() donenn");
fprintf(stderr, "### test Resource::alloc_shared()n");
{
std::shared_ptr<Resource> r3 = Resource::alloc_shared();
fprintf(stderr, "### test Resource::alloc_shared() allocatedn");
if (r3) { }
}
fprintf(stderr, "### test Resource::alloc_shared() donenn");
fprintf(stderr, "### test Resource::alloc_shared2()n");
{
std::shared_ptr<Resource> r3 = Resource::alloc_shared2();
fprintf(stderr, "### test Resource::alloc_shared2() allocatedn");
if (r3) { }
}
fprintf(stderr, "### test Resource::alloc_shared2() donenn");
fprintf(stderr, "### test Resource::alloc_opt()n");
{
std::experimental::optional<Resource> r4 = Resource::alloc_opt();
fprintf(stderr, "### test Resource::alloc_opt() allocatedn");
if (r4) { }
}
fprintf(stderr, "### test Resource::alloc_opt() donenn");
fprintf(stderr, "### test Resource::alloc_opt2()n");
{
std::experimental::optional<Resource> r4 = Resource::alloc_opt2();
fprintf(stderr, "### test Resource::alloc_opt2() allocatedn");
if (r4) { }
}
fprintf(stderr, "### test Resource::alloc_opt2() donenn");
fprintf(stderr, "### test Resource::create()n");
{
Resource r5(Resource::create());
fprintf(stderr, "### test Resource::create() createdn");
if (r5) { }
}
fprintf(stderr, "### test Resource::create() donenn");
fprintf(stderr, "### test Resource::create2()n");
{
Resource r5(Resource::create2());
fprintf(stderr, "### test Resource::create2() createdn");
if (r5) { }
}
fprintf(stderr, "### test Resource::create2() donenn");
}
  1. 使用NDEBUG定义的alloc_opt2和create2方法调用Resource的move构造函数。这在某种程度上可以避免吗

是的,这很容易,只需停止写入RETURN_X(std::move(r)),改为说RETURN_X(arg),其中宏调用std::move

#define RETURN_X return debug_x("%s = %pn", __PRETTY_FUNCTION__, std::move(arg));

现在,当NDEBUG被定义时,返回语句只是return arg;,所以您得到了move-elison。

当未定义NDEBUG时,您仍然像以前一样将返回值移动到调试函数中。

在非NDEBUG模式中,createcreate2alloc_optalloc_opt2中的move构造可能不会被删除(标准调用时,会删除),因为如果返回函数参数,则禁止删除。我尝试了很多,但没能找到一种透明、安全、无需移动地调试返回值的方法。将NRVO与在返回之前转储值相结合似乎是最好的主意,但这意味着,正如我所说,首先转储它,然后返回它,而不是通过调试转储函数传递它。

所以你有类似的东西

Resource r(access{});
DUMP_RESOURCE(r);  // disappears if NDEBUG
return r;
}

确保你没有

  • 将return语句放入块中(类似于do { } while(0)技巧)
  • 返回比变量名称更复杂的任何表达式(如std::move(r)(DUMP(r), r)

(编辑4:删除了这里被证明不太好用的想法,并解释了什么是有效的。)

在NDEBUG模式下,也没有独立于编译器的方法来消除移动。C++标准解释说,按值返回会导致移动或复制构造,可能会被忽略以优化程序,但有一种特殊情况除外:

return {access()};

仅在这种情况下,保证直接构建返回值。

如果在定义NDEBUG的所有情况下都启用了优化,我希望一个好的编译器能够删除move构造。在create的情况下,这被称为返回值优化(RVO),而在create2中,这被命名为返回值最优化(NRVO)。

在返回语句中使用std::move局部变量被认为不是一种好的风格,而且std::move在g++4.9上抑制了create2中的NRVO。不过,我确实理解,如果您通过调试函数传递该对象,则需要std::move;但在这里,您可以遵循Jonathan Wakely的建议,将std::move放入调试宏中,这对我来说也是个好主意。

(编辑3:我测试了最后一段中的陈述)


将函数创建的某个对象放入调用方的领域而不移动的一种解决方法是,您不将其返回,而是将其emplace放入调用方提供的std::experimental::optional,该函数在脱离状态下传入。

void Resource::workaround(std::experimental::optional<Resource> &output)
{
output.emplace(access{});
#ifndef NDEBUG
debug_print(*output);
#endif
}
// call site:
int main()
{
std::experimental::optional<Resource> result;
Resource::workaround(result);
// work with "*result" now
}

正如按值返回一样,结果是在main的堆栈上管理的,但您可以保证获得无移动构造。

(编辑2:在编辑1中添加了完全返工的变通方法)

启动一个新的ansare,因为这对注释来说太长了:

你可以试着按照的思路来解决这个问题

#define RETURN_A(arg) do { auto t = arg; 
debug_a("%s = %pn", __PRETTY_FUNCTION__, &t); 
return t; } while(0)

,因为在这种情况下,您将返回一个局部变量,其中副本可能会被忽略。(编辑:将x重命名为t,返回t并输出&t以匹配以前的RETURN_A)

这会导致以下错误:

debug.cc: In static member function 'static Resource Resource::create2()':
debug.cc:129:14: error: use of deleted function 'Resource::Resource(const Resource&)'
RETURN_A(r);
^
debug.cc:73:37: note: in definition of macro 'RETURN_A'
#define RETURN_A(arg) do { auto t = arg; fprintf(stderr, "%s = %pn", __PRETTY_FUNCTION__, &t); return t; } while(0)
^
debug.cc:25:5: note: declared here
Resource(const Resource &) = delete;
^

std::move(t)std::forward<decltype(t)>(t)都没有解决这个问题。