1
\$\begingroup\$

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?

\$\endgroup\$
1
\$\begingroup\$

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.

\$\endgroup\$
  • \$\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 Jul 10 '16 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 Jul 10 '16 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 and std::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 Jul 10 '16 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 Jul 10 '16 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 Jul 12 '16 at 0:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.