英文:
How to enforce in-place member construction for non-aggregate classes?
问题
有时,我希望在类的某些成员中强制执行原地构造(特别是非聚合类),以便可能提高效率,或者为了一种确定性感(“我对它做了控制”)。
然而,我无法做到。问题在于下面尝试实现作用域守卫时有所说明。
一个简单的实现将是这样。我希望隐藏 _func
和 _exec
,并禁用复制/移动构造/赋值,因此聚合体不是一个选项。
template<class F>
class [[nodiscard]] at_scope_exit {
F _func;
bool _exec = true;
public:
at_scope_exit(F&& f) noexcept(std::is_nothrow_move_constructible_v<F>):_func{ std::move(f) } {}
void disable() noexcept { _exec = false; }
at_scope_exit(const at_scope_exit&) = delete;
at_scope_exit& operator=(const at_scope_exit&) = delete;
~at_scope_exit() {
if (_exec) { _func(); }
}
};
void useit() {
int i = 1;
at_scope_exit atexit{ [&] {i--; } };
}
在大多数情况下它运行良好。然而,它不能保证原地构造。
struct bad_closure {
~bad_closure() { std::cout << "~bad_closure()"; }
void operator()() { std::cout << "operator()"; }
};
void problem() {
at_scope_exit atexit{ bad_closure{} };
//output: ~bad_closure()operator()~bad_closure()
}
撇开在实践中是否应该使用类似 bad_closure
的东西,构造然后移动的模式可能会让人感到不舒服。(在这种情况下,似乎编译器不允许进行拷贝省略)我如何消除这种可能性?
我应该如何修改 at_scope_exit
以便能够编写以下代码,而不用担心在构造时有额外的拷贝/移动?
void expected() {
struct foo { ~foo() { std::cout << "~foo()"; } };
int i = 1;
at_scope_exit atexit{ [&i, f = foo{}] { i = 0; } };
/*
required output:
~foo()
*/
}
英文:
Sometimes I hope to enforce in-place construction of some members in a class(especially a non-aggregate one), for possible efficiency gain, or for a sense of certainty("I have control over what it does").
However, I cannot manage to do so. The problem is illustrated in the following attempt to implement a scope guard.
A trivial implementation will be like this. I hope to hide _func
and _exec
, and disable copy/move construction/assignment, so an aggregate is not an option.
template<class F>
class [[nodiscard]] at_scope_exit {
F _func;
bool _exec = true;
public:
at_scope_exit(F&& f)noexcept(std::is_nothrow_move_constructible_v<F>):_func{ std::move(f) } {}
void disable()noexcept { _exec = false; }
at_scope_exit(const at_scope_exit&) = delete;
at_scope_exit& operator=(const at_scope_exit&) = delete;
~at_scope_exit() {
if (_exec) { _func(); }
}
};
void useit() {
int i = 1;
at_scope_exit atexit{ [&] {i--; } };
}
It works fine in most cases. However, it cannot guarantee in-place construction.
struct bad_closure {
~bad_closure() { std::cout << "~bad_closure()"; }
void operator()() { std::cout << "operator()"; }
};
void problem() {
at_scope_exit atexit{ bad_closure{} };
//output: ~bad_closure()operator()~bad_closure()
}
Leaving aside wether we should use something like bad_closure
in practice, this possibility of construct-then-move pattern makes me uncomfortable.(In this certain case, it seems the compiler is not allowed to do copy elision) How can I eliminate this possibility?
How should I modify at_scope_exit
to be able to write down the following code, without worrying about an extra copy/move on construction?
void expected() {
struct foo { ~foo() { std::cout << "~foo()"; } };
int i = 1;
at_scope_exit atexit{ [&i,f = foo{}] { i = 0; } };
/*
required output:
~foo()
*/
}
</details>
# 答案1
**得分**: 0
以下是您要翻译的内容:
只有在聚合初始化时才能获得复制/移动省略,而不能通过构造函数调用获得。
虽然您无法在聚合类中将复制/移动特殊成员函数声明为`delete`,但您可以通过添加一个不可复制/不可移动的成员来强制隐式声明的成员函数被定义为`delete`或根本不被声明。
例如:
struct non_movable {
non_movable() = default;
non_movable(const non_movable&) = delete;
non_movable& operator=(const non_movable&) = delete;
};
template<class F>
struct [[nodiscard]] at_scope_exit {
F _func;
class _exec_class : non_movable {
bool _value = true;
friend at_scope_exit;
} _exec{};
void disable() noexcept { _exec._value = false; }
~at_scope_exit() {
if (_exec._value) { _func(); }
}
};
这样,用户也无法访问`_exec`值。
但是,用户仍然可以访问`_func`对象。我认为在仍然保持聚合初始化的情况下,没有办法避免这一点。复制/移动省略无论如何都意味着对象在调用者的上下文中构造,因此它对其有完全控制权。
---
另一种选择是不直接传递`_func`对象,而是只传递构造对象所需的信息,然后在`at_scope_exit`的构造函数中就地构造它。
但只有在传递该信息比传递对象本身更便宜,或者`F`不可复制/不可移动时才有意义。
在您的示例中,复制/移动lambda表达式是便宜的,如果不是因为编译器不允许删除析构函数中的输出,生成的代码中可能没有实际成本。无论复制/移动省略规则如何,只要可观察的行为没有改变,编译器始终可以以任何方式进行优化。如果不是因为析构函数中的输出,对`closure`进行复制不会有任何可观察的行为差异。
我可以想到三种实现方法:
1. `at_scope_exit`的构造函数接受可变数量的参数,并将它们完美地转发以就地构造`F`对象:
template<typename... Args>
at_scope_exit(Args&&... args) : _func(std::forward<Args>(args)...) { }
2. `at_scope_exit`的构造函数接受一个可调用对象,该对象生成该对象:
template<std::invocable_r<F> U>
at_scope_exit(U&& u) : _func(std::invoke(std::forward<U>(u))) { }
3. `at_scope_exit`的构造函数接受可以转换为`F`的对象:
template<std::convertible_to<F> U>
at_scope_exit(U&& u) : _func(static_cast<F>(std::forward<U>(u))) { }
这些方法各有利弊。
在情况1和情况3中,用户需要显式提供类型`F`,而在情况2中,可能仍然可以编写一个类型推导指南,以使CTAD仍然能够工作。
可能在所有情况下,都有意义添加另一个参数作为标签,以允许仍然使用简单的复制/移动构造函数,并且明确定义,以便不将构造函数用作复制/移动构造函数。
(我假设C++20或更高版本。)
<details>
<summary>英文:</summary>
You can only get copy/move elision with aggregate initialization, not with a constructor call.
While you can't declare the copy/move special members functions as `delete`d in an aggregate class, you can enforce that the implicitly-declared member functions are defined as `delete`d or not declared at all by adding a member that is not copyable/movable.
For example:
struct non_movable {
non_movable() = default;
non_movable(const non_movable&) = delete;
non_movable& operator=(const non_movable&) = delete;
};
template<class F>
struct [[nodiscard]] at_scope_exit {
F _func;
class _exec_class : non_movable {
bool _value = true;
friend at_scope_exit;
} _exec{};
void disable() noexcept { _exec._value = false; }
~at_scope_exit() {
if (_exec._value) { _func(); }
}
};
This way it is also impossible for a user to access the `_exec` value.
However, it is still possible for a user to access the `_func` object. I don't think there is any way to avoid this while still maintaining aggregate initialization. Copy/move elision does anyway imply that the object is constructed in the context of the caller, so it has full control over it.
---
The other alternative is to not pass the `_func` object directly, but instead to pass only information needed to construct the object and then in-place construct it in a constructor of `at_scope_exit`.
But that only makes sense if passing that information would be cheaper than passing the object itself or if `F` isn't copyable/movable.
In your examples copying/moving the lambdas is cheap and there is probably no actual cost in the generated code if it weren't for your output in the destructor which the compiler is not allowed to remove. Regardless of copy/move elision rules the compiler is always allowed to optimize in any way as long as the observable behavior doesn't change. There wouldn't be any observable behavior difference between doing the copy of `closure` and not doing it if it weren't for the output in the destructor.
There are three ways to achieve this that I can think of:
1. A constructor for `at_scope_exit` which takes a variadic number of arguments and perfectly-forwards them to in-place construct the `F` object:
template<typename... Args>
at_scope_exit(Args&&... args) : _func(std::forward<Args>(args)...) { }
2. A constructor for `at_scope_exit` which takes a callable that produces the object:
template<std::invocable_r<F> U>
at_scope_exit(U&& u) : _func(std::invoke(std::forward<U>(u))) { }
3. A constructor for `at_scope_exit` which takes an object that can be converted to `F`:
template<std::convertible_to<F> U>
at_scope_exit(U&& u) : _func(static_cast<F>(std::forward<U>(u))) { }
These all have various pros and cons.
In cases 1. and 3. the user would need to provide the type `F` explicitly, while it would be possible to write a deduction guide for 2. that will still allow for CTAD to work.
Probably, in all cases, it would make sense to add a another parameter as a tag to allow the simple copying/moving constructor to still be used and clearly distinguished and so that the constructors aren't used as copy/move-like constructors.
(I am assuming C++20 or later.)
</details>
# 答案2
**得分**: 0
For your first case, you are passing a prvalue to `_func`, which requires 点击复制[1] for the compiler.
And the second case. In your constructor of `at_scope_exit_class`, you bind it to an rvalue reference (`f`). And your destructor of `closure` has observable side-effect. This prevents the compiler from optimizing.
For your question, your `closure` should have a constructor and destructor without any observable side-effect. And the compiler will be able to optimize.
[1]: https://en.cppreference.com/w/cpp/language/copy_elision
<details>
<summary>英文:</summary>
Assuming your are using C++17 or later.
For your first case, you are passing a prvalue to `_func`, which requires 点击复制[1] for compiler.
And the second case. In your constructor of `at_scope_exit_class`, you bind it to a rvalue reference(`f`). And your deconstructor of `closure` has observable side-effect. This prevents compiler from optimizing.
For your question, your `closure` should have constructor and deconstructor without any observable side-effect. And the compiler will be able to optimize.
[1]: https://en.cppreference.com/w/cpp/language/copy_elision
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论