SFML community forums

General => SFML development => Topic started by: unlight on April 14, 2020, 01:40:18 am

Title: SFML Move Semantics
Post by: unlight on April 14, 2020, 01:40:18 am
Hi guys,

I was hoping to find some information about the status of move-semantic implementation in SFML. I have seen a bit of discussion regarding C++ standard support and plans on the SFML roadmap, but I am not sure if any concrete decisions have been made, or if anyone is already working on it?

My motivation for asking is because I would be more than happy to work on implementing move semantics for SFML, but only if it fits with SFML's current plans and would be taken seriously / reviewed for merging.

Much love!
Title: Re: SFML Move Semantics
Post by: Laurent on April 14, 2020, 08:30:44 am
There was a big plan to switch code to a modern C++ standard (starting with writing some unit tests to ensure no regression), but obviously no one has enough free time and motivation to start doing it.

Move semantics don't break API compatibility, so this can be done for SFML 2.6 I guess. Let's do it ;)
Title: Re: SFML Move Semantics
Post by: unlight on April 18, 2020, 01:15:52 am
Alright, I will take this on then  :D

Before I start, I would just like to clarify my intentions would be to implement explicit move constructor and move assignment operators for classes that inherit from sf::NonCopyable.

Also, is there a place that the SFML devs usually discuss development when required, or is this forum post appropriate enough?

[EDIT]:

Oh gosh, I just noticed the lengthy discussion on sf::NonCopyable. Am I safe to assume we keep sf::NonCopyable and leave it untouched?

https://en.sfml-dev.org/forums/index.php?topic=21781.15 (https://en.sfml-dev.org/forums/index.php?topic=21781.15)
Title: Re: SFML Move Semantics
Post by: Laurent on April 18, 2020, 09:50:08 am
Quote
Am I safe to assume we keep sf::NonCopyable and leave it untouched?
Yes.
Title: Re: SFML Move Semantics
Post by: Elias Daler on April 24, 2020, 05:11:28 pm
Move semantics don't break API compatibility, so this can be done for SFML 2.6 I guess. Let's do it ;)
Hmm... but it requires a move to C++11. This is a huge change, it's not just about API?...
Title: Re: SFML Move Semantics
Post by: Laurent on April 24, 2020, 06:31:37 pm
Quote
Hmm... but it requires a move to C++11. This is a huge change, it's not just about API?...
What exactly is a huge change? To stop supporting non-C++11 compilers? I don't think it's that huge ;)
Title: Re: SFML Move Semantics
Post by: unlight on May 27, 2020, 02:04:48 am
I am currently postponing work on this feature in favour of something that is on the critical path to SFML 2.6. I shall return!
Title: Re: SFML Move Semantics
Post by: Nexus on June 18, 2020, 11:33:41 pm
Keep also the SFML Roadmap thread (https://en.sfml-dev.org/forums/index.php?topic=24372.0) and its ongoing discussions in mind, when providing additions possibly targeted to SFML 3.

Some things may still change :)


Quote from: Laurent
What exactly is a huge change? To stop supporting non-C++11 compilers? I don't think it's that huge ;)
I was thinking the same here (https://en.sfml-dev.org/forums/index.php?topic=21571.msg173548#msg173548), and maybe this is a part that should be discussed in the roadmap. The biggest risk I see is that SFML 3 is again several years away, and people have to wait even longer for useful additions like move semantics.

Projects that want to do "everything right" at once are often problematic. But as eXpl0it3r mentioned, SFML 3 might be rather short-lived, hoping that we can iterate faster on possible design mistakes.
Title: Re: SFML Move Semantics
Post by: unlight on June 20, 2020, 02:41:09 am
Alright, I am ready to start working on move semantics again, but a few conversations have happened around the place and I would like to make some suggestions about how to approach the problem.

Non-breaking move semantics
The initial plan is to implement move constructors and assignment operators only, and leave sf::NonCopyable to handle deleted copy functions. This way, the changes can roll straight in without API breakage. I assume that in the future, we may want to instead use =delete instead of sf::NonCopyable during a major update. This will be a much quicker job if the move semantics are already implemented.

Implement on a per-module basis
Implementing move semantics in SFML will not be trivial. The biggest risk is that I start working on it, make decent progress, but stop short due to lack of time or interest. Someone else might also try and do the same, and it becomes an endless ordeal. I suggest that we implement move semantics on a per-module basis. This would be less intimidating to work on and the job could be divided up for others to help with if it interests them.
Title: Re: SFML Move Semantics
Post by: Nexus on June 21, 2020, 01:12:12 pm
You mention some good points. I also think a modular approach makes more sense -- could even be on a class/group of classes basis, to have smaller incremental steps that can be reviewed in reasonable time.

I expect that after the release of SFML 2.6, the master branch will start targeting SFML 3 and new features will be merged against that. eXpl0it3r probably knows the approach in more details here.

So, what's left to decide is really:

are move semantics pressing enough to introduce them into SFML 2.x
or should we prioritize making a development branch available for SFML 3, so that PRs with new features can be processed?
Title: Re: SFML Move Semantics
Post by: Xiaohu on June 22, 2020, 02:18:39 pm
IMHO, the use of new C++ features, and more generally SFML codes changes are of two kinds:

- internal SFML code modifications that do not affect the library users (auto, decltype)
- modifications that affect user code (move semantics, destructuration... No or almost others?)

Of course, it's not that simple but some modifications can be 50/50 or more like in favor of one thing.

With any of the new C++ features, for me, the most important is move semantic and others are generally relevant to internal SFML code. The idea behind that is that internal modification is not critical but external changes or new features are, when the user code should change or, better, when new things are possible that were not possible previously. Use of auto inside of a method doesn't charge more for the user code, but move semantic is way more useful for the user, that can write code that he couldn't do earlier, and it's also only C++11 (so stable feature and no C++14, C++17 or +) so for me, it's the most important feature to implement. In addition, the code modifications needed are probably relatively small, and standard methods can probably be used. The small codes changes should however be tested because move semantics is (litteraly lol) a memory-critical feature :)

Another argument to implement move semantics: many new C++ features take advantage of move semantics. Of course, with this sort of argument, it's the snake biting its tail, but it should be noticed that even if SFML doesn't use C++11, probably like 90% of users do, and try to implement pre-C++11 code in an environment that probably use all that. So it's critical. And at least the others new features would maybe need to implement move semantics.

So for me: litteraly now ^^ (next release, that is SFML 2.x)
Title: Re: SFML Move Semantics
Post by: unlight on June 27, 2020, 07:21:25 am
Quote
You mention some good points. I also think a modular approach makes more sense -- could even be on a class/group of classes basis, to have smaller incremental steps that can be reviewed in reasonable time.

Adding the move semantics on a  class/group of classes basis is a great idea. As well as making review more managable, it also means more poeple can work on the task in parallel.

Quote
are move semantics pressing enough to introduce them into SFML 2.x
or should we prioritize making a development branch available for SFML 3, so that PRs with new features can be processed?

I am not fussed either way, I feel that adding move semantics in SFML 2.x would be in conjunction with sf::NonCopyable, where as SFML 3.0 is an opportunity to ditch sf::NonCopyable altogether. I am not whether a conclusion was ever reached on that discussion.

One last thing I would like to know, what module do you think would be most pressing to work on?
Title: Re: SFML Move Semantics
Post by: unlight on July 04, 2020, 07:48:14 am
There are two active PRs for move semantics on Github at the moment that have taken different approaches. I think this provides a good opportunity to discuss a method to standardise on.

Shaders
https://github.com/SFML/SFML/pull/1676
Performs move by swapping lhs object state with rhs object state.

Window
https://github.com/SFML/SFML/pull/1681
Performs move by destroying lhs object state, assuming rhs object state, and reinitialising rhs object state with sensible defaults.

I personally think that the first method is more prone to undesired behaviour. For instance, if the move constructor for a window was to perform a straight swap, then both windows would remain open after the operation completes. Any thoughts? Should we push for a consistent approach for move semantics?

[Edit]:
I found a discussion on the topic from SO.
https://stackoverflow.com/questions/6687388/why-do-some-people-use-swap-for-move-assignments
Title: Re: SFML Move Semantics
Post by: Nexus on July 04, 2020, 11:10:12 am
First, there are different levels of exception guarantees, specifying how involved objects behave in the presence of exceptions:

There are several idiomatic ways to implement the special member functions with C++11, to sum up:

1. Rule Of Five
Overload copy/move ctor, copy/move op= and dtor.
This is the "stupid" approach which requires a big amount of boilerplate, code duplication and thus is rather prone to errors when extending a class.

2. Copy-and-Swap
Overload all 5 special methods + swap(). Use swap() in copy/move op=.
Avoids some code duplication, but still a lot of boilerplate.
Achieves strong exception guarantee (each object is always in a valid state).

3. Rule of Four-and-a-half (https://stackoverflow.com/q/45754226)
Overload copy/move ctor and dtor as usual.
Overload op= taking a value (not const- or rvalue-reference), which means it acts as both move and copy assignments.

4. Rule of Zero
Let the compiler generate the Big Five.
Code complexity wise, this is the best solution, as the behavior is clear and no member can be forgotten. It does not work when there are complex ownership rules, or custom copy logic (for handles).
I've found my smart pointer aurora::CopiedPtr (https://bromeon.ch/libraries/aurora/tutorials/v1.0/smartptr.html) to be a very nice fit for pointers which require deep copies. It can easily reduce the number of methods needed from 5 to 0. For example, in Thor I need it for quite a few things (https://github.com/Bromeon/Thor/blob/f2cd8e3d725559e0b658153c656dfd8bfc9aec20/include/Thor/Animations/Detail/PlaybackSchemes.hpp#L102).
Downside: with compiler-generated member-wise copy, the exception guarantee is often reduced to basic (when the copy operation aborts in the middle).

I'm wondering if strong exception guarantee is something we really have to go for in SFML. Generally, I'm more in favor of readable/compact code. Having to implement the Big Five for every 2nd class is also often an indication of bad ownership design or lack of internal abstraction (too many types have custom ownership rules).
Title: Re: SFML Move Semantics
Post by: unlight on July 05, 2020, 10:36:30 am
I personally don't have a preference for how we approach move semantic support. I agree that manually writing the big 5 for each class is undesirable and I am also in support of concise, readable code. However, I really want behaviour to be logical, and I don't think that swapping internal state for resources like windows during move operations is logical.

A note worth mentioning is that if the internal handles were unique pointers then default move constructors would be perfect. Maybe this is a more valid approach for move semantics in some cases.

Ultimately, I am happy to work on this job in what ever way you more senior members think is best. We just need to be consistent. Please, contribute to the discussion and be harsh on the PRs. I will do the grunt work ;)
Title: Re: SFML Move Semantics
Post by: Nexus on July 05, 2020, 12:46:57 pm
However, I really want behaviour to be logical, and I don't think that swapping internal state for resources like windows during move operations is logical.

A classical implementation of Copy-and-Swap would be like this:
class Image
{
        Image();
        Image(const Image& copied);
        Image(Image&& moved);

        void swap(Image& other);

        Image& operator= (const Image& copied) {
                Image tmp(copied);
                tmp.swap(*this);
                return *this;
        } // object 'tmp' (with previous contents of *this) is destroyed

        Image& operator= (Image&& moved)
        {
                moved.swap(*this);
                return *this;
        } // object 'moved' (with previous contents of *this) is destroyed
};

Since the temporary object is destroyed after a copy operation, it does not matter what it contains. A moved-from object is typically destroyed after the move as well, but there are exceptions like std::unique_ptr which define a behavior. So, unless SFML would explicitly guarantee that a moved-from image is equal to an empty image, we can put any valid content into moved-from objects. Otherwise we would simply involve a default-constructed object to "reset" the image after moving.

The reason why this idiom exists in the first place, is because the naive (or compiler-generated) implementation does this:
        Image& operator= (const Image& copied)
        {
                member0 = copied.member0;
                member1 = copied.member1;
                // ...
                memberN = copied.memberN;

                return *this;
        }

        Image& operator= (Image&& moved)
        {
                member0 = std::move(moved.member0);
                member1 = std::move(moved.member1);
                // ...
                memberN = std::move(moved.memberN);
       
                return *this;
        }

Now imagine that during "...", an exception occurs while copying/moving. The assignment operation is aborted, but both objects are in an inconsistent, "half-moved" or "half-copied" state. This is mostly a problem for copies, as moves should be designed to not fail (however, this is not always possible, e.g. when moves fall back to copies, for raw arrays for example).

What Copy-and-Swap does is -- instead of overwriting the object in-place -- first creating a new valid object, swapping with that and then destroying the old one. Since both swap and destroy operations should not fail, only the construction of the new object can fail, in which case the object is left in the previous, valid state.




So much about the theory. What I mentioned above is absolutely crucial when designing generic building blocks like containers, smart pointers, graphs or other data structures. You don't know what the user will put inside, so you have to assume copies can fail, and design the code accordingly.

Now, in practice, you often have better knowledge about what types are involved, and can exploit this knowledge to simplify code. If SFML resorts to not throwing exceptions during copy/move operations, and has no APIs where user-defined types could trigger exceptions, we can avoid the extra effort and try to use the compiler-generated methods where possible.

Pragmatically, I would attempt to use default move (and even copy) constructors/assignment operators wherever possible -- it's just so much easier to reason about code, to add new fields, etc. But since SFML operates with resources in several places, it involves custom ownership semantics here and there. These are the cases we need to analyze and determine what is the greatest common denominator.
Title: Re: SFML Move Semantics
Post by: Nexus on July 05, 2020, 01:25:15 pm
A note worth mentioning is that if the internal handles were unique pointers then default move constructors would be perfect. Maybe this is a more valid approach for move semantics in some cases.
In sf::Shader, the private members are those:

    ////////////////////////////////////////////////////////////
    // Types
    ////////////////////////////////////////////////////////////
    typedef std::map<int, const Texture*> TextureTable;
    typedef std::map<std::string, int> UniformTable;

    ////////////////////////////////////////////////////////////
    // Member data
    ////////////////////////////////////////////////////////////
    unsigned int m_shaderProgram;  //!< OpenGL identifier for the program
    int          m_currentTexture; //!< Location of the current texture in the shader
    TextureTable m_textures;       //!< Texture variables in the shader, mapped to their location
    UniformTable m_uniforms;       //!< Parameters location cache

The two std::maps would move fine (emptying the source), but the integers will be copied. So, the default move implementation would leave a half-working instance behind.

As mentioned, in SFML we need to decide whether we give guarantees on moved-from objects or not.
For the sake of least surprise and simplicity, it might be more user-friendly to make moved-from objects behave like default-constructed ones.

In that regard, your current implementation is inconsistent:
sf::Shader a = ...;
sf::Shader b = std::move(a); // a is now empty
however:
sf::Shader a = ...;
sf::Shader b = ...;
b = std::move(a) // a now has contents of b

Since you already implement Move-and-Swap (but backwards), you might as well reuse swap() to always empty the source. In general, it's better to call the copy/move constructor in the copy/assignment operator, not vice versa.

A generic approach is this. It does a bit more than necessary, but this pattern can be applied everywhere, and the advantage is that the actual logic is in the swap() and only there --- move ctor/op= don't need to be changed when the fields change.
class Shader
{
    Shader();
   
    Shader(Shader&& moved)
    : Shader()
    {
        moved.swap(*this); // empties source
    }

    Shader& operator= (Shader&& moved)
    {
        Shader tmp(std::move(moved)); // invoke move ctor, empties 'moved'
        tmp.swap(*this);
        return *this;
    } // destroy 'tmp',

    void swap(Shader& other); // actual logic is here
}

We should also establish a naming convention for parameter names. For copy constructors, SFML currently uses copy. This is slightly misleading, as the parameter is the object being copied (origin) and not the copy (replica). In Thor I used origin and source, but I think we can be more precise.

What about copied and moved?
This makes it immediately clear that you're dealing with a copy or move ctor/op=.



[Edit] I made an example how I would probably implement it here:

https://github.com/SFML/SFML/tree/feature/move-semantics

One commit with unified copy/move op=, the whole branch diff with separate ones.
Not tested -- those things might be a candidate for unit tests.
Title: Re: SFML Move Semantics
Post by: unlight on July 07, 2020, 02:21:27 am
Thank you for the theory Nexus, I have not been exposed to the copy-and-swap pattern before so it is interesting to see why and when it could be useful.

After reading through your response and seeing your example implementation, how does this sound as the implementation strategy going forward?


Considering you have now created the move semantics branch, should we consider your texture implementation as the first official commit and PRs be accepted when ready to this branch?
Title: Re: SFML Move Semantics
Post by: Nexus on July 09, 2020, 10:27:53 pm
Those suggestions sound good. There are fortunately many classes in SFML, for which the default semantics work well.

But we need to be careful: sf::Image for example requires no special member functions at the moment (although there's a destructor doing nothing (https://github.com/SFML/SFML/blob/master/src/SFML/Graphics/Image.cpp#L48-L52)). However, a class with no special member functions declared is subject to implictly generated special member functions (aka Big Five). This would break in C++11, as a newly introduced move constructor does a member-wise move of each field, leaving the std::vector and sf::Vector2u fields inconsistent to each other. Ironically, the no-op destructor -- since declared -- prevents exactly that: sf::Image has no move ctor, even if compiled with a C++11 compiler.

Regarding my PR, I'd like to get some feedback, also from other  SFML team members ;)
Title: Re: SFML Move Semantics
Post by: Nexus on July 26, 2020, 11:20:36 am
Regarding my PR, I'd like to get some feedback, also from other  SFML team members ;)

Anyone? :)
(also SFML users, not just team)
Title: Re: SFML Move Semantics
Post by: Xiaohu on August 01, 2020, 11:43:49 pm
I can be biaised by what I need, but I suggest to first add move semantic for simple classes, that is which do not need custom logic.

Which method to use may be important for consistency, but maybe there should be no more than few tens of classes that supports move semantics, so rewriting style from one method to another may take not long.

Most of the work is to identify which classes need custom move logic (I think mostly Handles/Resources), and which ones don't. In this case, it should be possible to make a prototype to follow about the style and conventions, edge-cases classes could be modified after.
Title: Re: SFML Move Semantics
Post by: Nexus on August 03, 2020, 11:10:48 pm
I can be biaised by what I need, but I suggest to first add move semantic for simple classes, that is which do not need custom logic.
For those, the compiler-generated move constructor/operator= would already do the correct thing, or am I misunderstanding you?

Which method to use may be important for consistency, but maybe there should be no more than few tens of classes that supports move semantics, so rewriting style from one method to another may take not long.
Yeah, since there hasn't been much feedback so far, I would suggest we choose one style and go for it. Doing it consistently still makes sense though. It's confusing for everyone if every move ctor/op= is implemented with a different idiom, and it creates needless work to refactor.
Title: Re: SFML Move Semantics
Post by: unlight on August 13, 2020, 01:55:09 am
SFML Devs: Can we please get some final input on the topic, or start making some progress on the PRs? Not sure if there is anything else I can do.
Title: Re: SFML Move Semantics
Post by: Xiaohu on August 20, 2020, 01:47:30 am
I guess everybody including SFML Devs has holidays ^^
Title: Re: SFML Move Semantics
Post by: reconn on December 05, 2021, 10:04:14 pm
If I may introduce my point and suggestions.

First of all, I agree with copy-and-swap in advance.

Second of all, C++ programmers often make the mistake of supplying classes with too much noise (defining empty destructors, implementing default copy/move constructors, and default assignment operator overloads) which does more harm than good for readability and maintainability.

The rule of thumb should be to default to the Rule of Zero. Only when it does not suffice, fallback to the Rule of Three or Five (preferably) starting with compiler defaults ("= default") or Four-and-a-Half even. It is most intuitive when the noise is removed. An empty non-virtual destructor makes the class non-trivially destructible from the Standard's point of view. A non-noexcept move constructor makes the class non-no-throw-move-constructible.

The question could come: why be strict about the noexcept specifier in move semantics? Because STL utilizes this mechanic AND most importantly it assures that the move construction/assignment is intuitive. If a class can not afford a noexcept move constructor/assignment then it is unable to simply/intuitively transfer its states thus should not use move semantics but a transfer mechanism of some sort. A move constructor/assignment should only do simple transfers. No heap allocations, no checks, no side-effects. Only then it is intuitive.

Should a class need a "move semantic" function with a possible exception - one could write a member function for that purpose and not utilize move constructors/assignment operators.

// Ways of transferring object states in std::unique_ptr
auto ptr = std::make_unique<int>(7);
// a) "Move semantic" transfer
auto otherPtr = std::move(ptr);
// b) A transfer mechanism which does not need to impose noexcept
auto anotherPtr = std::unique_ptr<int>(ptr.release());
 

Of course, this is an example of std::unique_ptr which has noexcept `release()` function. However, this is just to show how it can be accomplished/worked around to keep a strong exception guarantee on move semantics.