In some code that I am writing, I am manipulating some heterogeneous containers of user-defined types.
I desperately need to be able to move-assign these types without generating exceptions.
However, this can cripple the usability of my code because on many older compilers, important standard library types do not have noexcept
markings on their move-assignment operators. For instance std::string
appears not to be noexcept
move-assignable with gcc 4.9, it is only noexcept
move constructible in C++11.
To work around this, I have developed a template function which tries various strategies for move-assigning things until it finds one which is noexcept
, if it cannot then it gives up.
namespace traits {
template <bool b, typename V = void>
using enable_if_t = typename std::enable_if<b, V>::type;
} // end namespace traits
namespace detail {
template <typename T>
auto move_assign_noexcept(T & dest, T && src) noexcept -> traits::enable_if_t<std::is_nothrow_move_assignable<T>::value> {
dest = std::move(src);
}
template <typename T, typename ENABLE = void>
struct is_nothrow_swappable : std::false_type {};
template <typename T>
struct is_nothrow_swappable<T, traits::enable_if_t<noexcept(std::swap(*static_cast<T*>(nullptr), *static_cast<T*>(nullptr)))>> : std::true_type {};
template <typename T>
auto move_assign_noexcept(T & dest, T && src) noexcept -> traits::enable_if_t<!std::is_nothrow_move_assignable<T>::value
&& is_nothrow_swappable<T>::value> {
std::swap(dest, src);
}
template <typename T>
auto move_assign_noexcept(T & dest, T && src) noexcept -> traits::enable_if_t<!std::is_nothrow_move_assignable<T>::value
&& !is_nothrow_swappable<T>::value
&& std::is_nothrow_move_constructible<T>::value> {
dest.~T();
new (&dest) T(std::move(src));
}
} // end namespace detail
This code is supposed to work at C++11 standard.
I recall that there is a standard function std::move_if_noexcept
which is useful for standard containers which are trying to avoid exception problems.
Is there an existing standard idiom for doing what I am trying to do?
Are there other strategies that I should try? Am I actually using standard traits correctly to ensure that each implementation actually won't throw?
1 Answer 1
I think you're putting the cart before the horse here.
I am manipulating some heterogeneous containers of user-defined types. I desperately need to be able to move-assign these types without generating exceptions.
Let's say your container is a Cart<T>
. And you need to define a move-assignment operator
Cart<T>& Cart<T>::operator=(Cart<T>&& rhs) noexcept;
Well, if Cart
behaves like std::vector
, then you could implement this as something like
Cart<T>& Cart<T>::operator=(Cart<T>&& rhs) noexcept
{
delete [] this->pointer_to_Ts;
this->pointer_to_Ts = rhs->pointer_to_Ts;
rhs->pointer_to_Ts = nullptr;
}
Notice that this does not require T
's own move-assignment operator at all; T
might not even be movable! The only user-defined function we're executing here is T
's destructor (and if that throws, we're in trouble anyway).
But okay, let's suppose that you're implementing Cart
's move-assignment operator like this instead:
Cart<T>& Cart<T>::operator=(Cart<T>&& rhs) noexcept
{
for (int i=0; i < n; ++i) {
this->array_of_Ts[i] = std::move(rhs->array_of_Ts[i]);
}
}
So now you're calling a ton of user-defined functions (well, just one function, but you're calling it n
times). This is already violating the spirit of move-semantics, in that moving is supposed to be a fast operation, but we'll let that slide.
What should you do if T
's move-assignment operator is defined as noexcept
? Well, you're golden. Everything will work fine.
What should you do if T
's move-assignment operator is not defined as noexcept
, but doesn't throw in practice? Well, you're golden. Everything will work fine in practice. If it ever accidentally does throw an exception, then your program will call std::terminate
and (presumably) exit. This seems like a good idea in most cases. If you really can't afford to call std::terminate
when something as terrible as "move-assignment throws an exception" happens, then I bet you're coding to some strict government standards that will preclude the existence of non-noexcept
move-assignment operators in the first place.
What should you do if T
's move-assignment operator is explicitly defined as noexcept(false)
? Well, you should probably go find the person who wrote that code and shoot them in the face. (Politely, of course.)
Now the unfortunate bit: It's actually impossible to tell in C++14 (and C++17 as well, AFAIK) whether a given function has been declared noexcept(false)
or merely non-noexcept
. So your code won't ever be able to distinguish case 2 above ("you're golden, in practice") from case 3 above ("you have to shoot somebody in the face") — it's useless to try.
So, should you go gray worrying about case 3? IMHO you should not. Just assume that every instance of case-2-or-3 in your codebase is really a case-2; and then write your code accordingly. It'll be simple and straightforward and efficient, and if you ever run into a throwing move-assignment operator, well, by definition it's the user who wrote the bad code, not you.
This is not the philosophy that led to the current pessimal behavior of std::vector::resize
. I'll let someone else take a stab at explaining that philosophy, because I don't understand it at all.
-
\$\begingroup\$ So, unfortunately I can't just move the container, I have a
std::array<T, N>
on my hands and I need to move it. I could send you a link into my actual project I guess. \$\endgroup\$Chris Beck– Chris Beck2016年07月10日 10:52:27 +00:00Commented Jul 10, 2016 at 10:52 -
\$\begingroup\$ The reason I don't want to throw is that, this is supposed to be a lua bindings library, and if the user throws exceptions through lua and catches them on the other side, that's basically a UB. lua is a C library, not exception safe. And it's uniquely tricky because, when you use lua, you are taking C++ functions with signature
int(lua_State *)
and passing those function pointers to the C lib so that it cna call them as callbacks. So the exceptions really will go right through lua's internals. \$\endgroup\$Chris Beck– Chris Beck2016年07月10日 10:54:05 +00:00Commented Jul 10, 2016 at 10:54 -
\$\begingroup\$ I like your idea to just make the function
noexcept
though, I think that is basically correct. I shoudl just tell the user it has to be nothrow move assignable. Even if they are using gcc 4.9 andstd::string
is technically not that, in reality, it is I guess. I would like to make it a static assert so that if they do something stupid they get a message rather than a crash, but that might just be unworkable i guess. There probably aren't any "real" classes that are no throw move contructible and destructible but not no-throw move assignable, so this whole exercise may be silly. \$\endgroup\$Chris Beck– Chris Beck2016年07月10日 10:55:32 +00:00Commented Jul 10, 2016 at 10:55 -
\$\begingroup\$ Note: To clarify: in one case i have a
std::array<T,N>
, in one case, it's some user-defined struct, and I need to default consturct it, and then move values into its fields one by one. In both cases the move assignment is the tricky part. github.com/cbeck88/lua-primer/blob/master/include/primer/… github.com/cbeck88/lua-primer/blob/master/include/primer/… \$\endgroup\$Chris Beck– Chris Beck2016年07月10日 10:59:35 +00:00Commented Jul 10, 2016 at 10:59 -
\$\begingroup\$ Sorry, those links were to "master" and not to a specific commit unfortunately, so the highlighted line has drifted... oh well \$\endgroup\$Chris Beck– Chris Beck2016年07月12日 00:35:26 +00:00Commented Jul 12, 2016 at 0:35
Explore related questions
See similar questions with these tags.