【问题标题】:How to avoid the move constructor in debug macros?如何避免调试宏中的移动构造函数?
【发布时间】:2015-01-23 23:24:48
【问题描述】:

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

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

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

现在这是我的问题:

  1. 使用定义了 NDEBUG 的 alloc_opt2create2 方法调用资源的移动构造函数。可以通过某种方式避免这种情况吗?
  2. 当未定义 NDEBUG 时,alloc_optcreate 方法也会调用 Resource 的移动构造函数。我真的想至少消除这些动作,这样无论有没有 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 = %p\n", __PRETTY_FUNCTION__, arg);
#define RETURN_B(arg) return debug_b("%s = %s\n", __PRETTY_FUNCTION__, arg);
#define RETURN_A(arg) return debug_a("%s = %p\n", __PRETTY_FUNCTION__, arg);

#endif

Resource * Resource::alloc_ptr() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_P(new Resource(access{}));
}

Resource * Resource::alloc_ptr2() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    Resource * r = new Resource(access{});
    RETURN_P(r);
}

std::unique_ptr<Resource> Resource::alloc_uniqe() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_P(std::make_unique<Resource>(access{}));
}

std::unique_ptr<Resource> Resource::alloc_uniqe2() {
    fprintf(stderr, "%s\n", __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, "%s\n", __PRETTY_FUNCTION__);
    RETURN_P(std::make_shared<Resource>(access{}));
}

std::shared_ptr<Resource> Resource::alloc_shared2() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    std::shared_ptr<Resource>  r = std::make_shared<Resource>(access{});
    RETURN_P(r);
}

std::experimental::optional<Resource> Resource::alloc_opt() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_P(std::experimental::optional<Resource>(std::experimental::in_place, access{}));
}

std::experimental::optional<Resource> Resource::alloc_opt2() {
    fprintf(stderr, "%s\n", __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, "%s\n", __PRETTY_FUNCTION__);
    RETURN_A(Resource(access{}));
}

Resource Resource::create2() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    Resource r = Resource(access{});
    RETURN_A(std::move(r));
}

Resource::Resource(access) : valid(true) {
    fprintf(stderr, "%s\n", __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, "%s\n", __PRETTY_FUNCTION__);
    assert(other.valid);
    other.valid = false;
}

Resource::operator bool() const {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_B(valid);
}

int test_int() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN("%d", 1);
}

int main() {
    fprintf(stderr, "### test test_int()\n");
    {
    int i = test_int();
    fprintf(stderr, "### test test_int() created\n");
    if (i) { }
    }
    fprintf(stderr, "### test test_int() done\n\n");

    fprintf(stderr, "### test Resource::alloc_ptr()\n");
    {
    Resource * r1 = Resource::alloc_ptr();
    fprintf(stderr, "### test Resource::alloc_ptr() allocated\n");
    delete r1;
    }
    fprintf(stderr, "### test Resource::alloc_ptr() done\n\n");

    fprintf(stderr, "### test Resource::alloc_ptr2()\n");
    {
    Resource * r1 = Resource::alloc_ptr2();
    fprintf(stderr, "### test Resource::alloc_ptr2() allocated\n");
    delete r1;
    }
    fprintf(stderr, "### test Resource::alloc_ptr2() done\n\n");

    fprintf(stderr, "### test Resource::alloc_unique()\n");
    {
    std::unique_ptr<Resource> r2 = Resource::alloc_uniqe();

    fprintf(stderr, "### test Resource::alloc_uniqe() allocated\n");
    if (r2) { }
    }
    fprintf(stderr, "### test Resource::alloc_uniqe() done\n\n");

    fprintf(stderr, "### test Resource::alloc_unique2()\n");
    {
    std::unique_ptr<Resource> r2 = Resource::alloc_uniqe2();

    fprintf(stderr, "### test Resource::alloc_uniqe2() allocated\n");
    if (r2) { }
    }
    fprintf(stderr, "### test Resource::alloc_uniqe2() done\n\n");

    fprintf(stderr, "### test Resource::alloc_shared()\n");
    {
    std::shared_ptr<Resource> r3 = Resource::alloc_shared();
    fprintf(stderr, "### test Resource::alloc_shared() allocated\n");
    if (r3) { }
    }
    fprintf(stderr, "### test Resource::alloc_shared() done\n\n");

    fprintf(stderr, "### test Resource::alloc_shared2()\n");
    {
    std::shared_ptr<Resource> r3 = Resource::alloc_shared2();
    fprintf(stderr, "### test Resource::alloc_shared2() allocated\n");
    if (r3) { }
    }
    fprintf(stderr, "### test Resource::alloc_shared2() done\n\n");

    fprintf(stderr, "### test Resource::alloc_opt()\n");
    {
    std::experimental::optional<Resource> r4 = Resource::alloc_opt();
    fprintf(stderr, "### test Resource::alloc_opt() allocated\n");
    if (r4) { }
    }
    fprintf(stderr, "### test Resource::alloc_opt() done\n\n");

    fprintf(stderr, "### test Resource::alloc_opt2()\n");
    {
    std::experimental::optional<Resource> r4 = Resource::alloc_opt2();
    fprintf(stderr, "### test Resource::alloc_opt2() allocated\n");
    if (r4) { }
    }
    fprintf(stderr, "### test Resource::alloc_opt2() done\n\n");

    fprintf(stderr, "### test Resource::create()\n");
    {
    Resource r5(Resource::create());
    fprintf(stderr, "### test Resource::create() created\n");
    if (r5) { }
    }
    fprintf(stderr, "### test Resource::create() done\n\n");

    fprintf(stderr, "### test Resource::create2()\n");
    {
    Resource r5(Resource::create2());
    fprintf(stderr, "### test Resource::create2() created\n");
    if (r5) { }
    }
    fprintf(stderr, "### test Resource::create2() done\n\n");
}

【问题讨论】:

    标签: c++ c++11 constructor move


    【解决方案1】:
    1. 使用定义了 NDEBUG 的 alloc_opt2 和 create2 方法调用 Resource 的移动构造函数。可以通过某种方式避免这种情况吗?

    是的,这很简单,只需停止编写 RETURN_X(std::move(r)) 并改为使用 RETURN_X(arg),而宏调用 std::move

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

    现在,当定义 NDEBUG 时,返回语句只是 return arg;,所以你会得到移动省略。

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

    【讨论】:

      【解决方案2】:

      在非 NDEBUG 模式下,createcreate2alloc_optalloc_opt2 中的移动构造可能不会被删除(省略,正如标准所称),因为这如果返回函数参数,则禁止省略。我努力尝试,但未能找到一种方法来透明地调试返回值,使用安全且无需移动。将 NRVO 与在返回之前转储值结合起来似乎是最好的主意 - 但这意味着,正如我所说 first 转储它,then 返回它,而不是传递它 通过调试转储函数。

      所以你有类似的东西

          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 并且在所有情况下都启用了优化,我希望一个好的编译器能够删除移动构造。对于create,这称为返回值优化(RVO),而在create2 中,这称为命名返回值优化(NRVO)。

      在 return 语句中 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 中添加了完全重新设计的解决方法)

      【讨论】:

      • “我明白,如果你通过调试函数传递该对象,你需要 std::move。” std::move 可以放在宏中不过,所以 RETURN_A(arg); 扩展为 return arg;return debug_a("%s = %p\n", __PRETTY_FUNCTION__, std::move(arg));
      • "从函数中获取值到调用者管理的位置的解决方法是通过非常量引用或指针将 std::experimental::optional 传递给函数并进行替换将返回值传入其中。如果需要,您可以在之后通过引用调试打印函数将其传递。"
        这是调试某个函数具有的任何返回值。所以如何退货不取决于我。它必须适用于任何事情。
      • @JonathanWakely 这会在调试案例中直接移动所有移动构造函数。人们可以称之为进步,但它使调试和非调试模式之间的差异更大。 :(
      • @MichaelKracher 我使用的是 g++-4.9 -O2,通常优化得很好。
      • #define RETURN_A(arg) do { auto x = arg; debug_a("%s = %p\n", __PRETTY_FUNCTION__, x); return arg; } while(0) 您不能对arg 进行两次评估。你的意思是return x;
      【解决方案3】:

      开始一个新的答案,因为这对于 cmets 来说太长了:

      您可以尝试按照以下方式解决此问题

      #define RETURN_A(arg) do { auto t = arg; \
          debug_a("%s = %p\n", __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 = %p\n", __PRETTY_FUNCTION__, &t); return t; } while(0)
                                           ^
      debug.cc:25:5: note: declared here
           Resource(const Resource &) = delete;
           ^
      

      std::move(t)std::forward&lt;decltype(t)&gt;(t) 都不能解决这个问题。

      【讨论】:

      • 对不起,我昨天没有看到你的额外答案。这种方法不适用于 g++-4.9,我将相应地编辑我的答案。此外,我在尝试 NRVO 时偶然发现了一些可能是 NRVO 的错误。获取局部变量的地址并对其进行 NRVO 处理并不能很好地结合在一起。
      • 我撤回了我声称的关于 gcc 中的错误的任何内容。这是椅子和键盘之间的错误。从先前的评论中编辑该部分为时已晚。另一方面,如果这个答案和我的两个 cmets 被删除,我根本不会被打扰。
      • 可以将变量的地址打印出来是额外动作的原因吗?就像防止 NRVO 合法化一样。
      • 没有。获取地址并不会阻止 NRVO,我在为我的答案编写最后一次编辑时对其进行了测试。 &t 实际上是调用者提供的空间的地址,尽管它在语法上是被调用者的成员。
      猜你喜欢
      • 2016-04-26
      • 1970-01-01
      • 1970-01-01
      • 2021-10-09
      • 1970-01-01
      • 2013-04-09
      • 2017-03-17
      • 2018-09-19
      • 1970-01-01
      相关资源
      最近更新 更多