Skip to content
Lukas Dürrenberger edited this page Jul 20, 2021 · 9 revisions

The main goal of SFML 3 is to bring C++17 support to SFML. Since C++98/03 the language has evolved a lot, as such this solution design tries to cover as many potential API specific improvements as possible.

Notes

  • This is not about how SFML's internal code can be refactored, but about topics that have a direct effect on the public SFML API. There's a small chapter about implementation at the end of this page, which might get split at a later point.
  • If you want to discuss any of the mentioned or missing topics in-depth, please check for existing threads or open a new one on the forum.

Current Situation

SFML is already over 13 years old and as such was born before C++11 was a thing. For SFML 2 it was decided to stick to the current C++03 standard used. Unfortunately the development of SFML has stagnated quite a bit and the release of the next major version has been delayed for many years. The situation is rather critical, as pretty much every project out there is using at least C++11 and many are already on C++17 waiting for complete tooling support for C++20. The major pain point with regards to the API is the missing move semantics.

Target Situation

As mentioned, the focus is on the API level and not the SFML-internal refactorings.

There are multiple levels when it comes to the different feature discussions:

  • Required: API changes that are expected to properly operate and integrate with other code
  • Standardized: The feature was added to the standard, as such is obsolete and should be removed
  • Modernized: The new standards offer new API design possibilities
  • Helper: While the standard or SFML provides an API, for usability and the most common cases, a helper would be useful

Generally, we would like to be pragmatic in the approach, meaning we will migrate to C++11/14/17 features where it makes sense and improves easy-of-use or performance significantly. We will not try to follow every possible trend; being part of a newer language standard alone does not justify the use of a feature.

Table of Contents

General Topics

C++ Standard

Decision: SFML 3 will use C++17

Many years ago, the original proposal focused on supporting C++11 and maybe C++14, but given the past time and adding the time until SFML 3 is released, the argument is a lot stronger for supporting C++17.

Cons

  • Surveys from 2019 [1][2] only show a C++17 adoption of 10-30%
  • Some platforms may not be supported anymore

Pros

  • Survey from 2021 [1] shows a majority adoption (42-49%) of C++17 (60-75% for C++17 and C++20)
  • By the time SFML 3 is released, the C++11 and C++14 will already feel very dated
  • The C++17 standard brings lot more options to modernize the API
  • Android and iOS already support C++17

See also the forum thread

CMake Changes

  • The new standard can be platform and toolchain independently requested with set(CMAKE_CXX_STANDARD 17)
  • There shouldn't be any additional changes required

C++ Features

Consult this list for an overview over different C++ standards, their language and library features.
Note that exceptions and error handling are a separate discussion, mostly orthogonal to the used C++ standard.

Move Semantics

Forum thread | #1675, PR #1676

Scope: add move constructor and move assignment operator to classes which either:

  • heavily benefit from the added performance (such as resources)
  • are currently non-copyable, but can be movable Do not add them when move semantics do not significantly improve anything, or the compiler-generated methods work just fine.

Implementation: avoid code duplication and unnecessary boilerplate, even at the chance of missed micro-optimizations (such as pointer copies).

One possibility is the following. Disadvantage is that move assignment is not noexcept if it can be copy as well. This may not be relevant for SFML however.

class MyClass()
{
    // Manual implementation
    MyClass(const MyClass& copied);
    MyClass(MyClass&& moved);
    ~MyClass();

    // Swap memberwise, exception-safe
    void swap(MyClass& other) noexcept;

    // By-value operator= covers both copy/move assignment
    MyClass& operator= (MyClass source)
    {
        source.swap(*this);
        return *this;
    } // destroy source
};

Alternative: split assignment operator into move and copy assignment. Benefit is noexcept move assignment.

Alternative: use a smart pointer such as aurora::CopiedPtr for fields that need copy/move semantics. Also supports deep copies, and allows the compiler-generated methods to do the right thing.

Multithreading API

Forum thread

Scope: Remove sf::Thread, sf::Mutex, sf::Lock, sf::ThreadLocal, sf::ThreadLocalPtr and replace them with their standard counterparts.

Chrono API

Forum thread

Scope: Keep sf::Clock and sf::Time in the API. Provide interoperability of sf::Time with std::chrono::duration.
If platform support is excellent meanwhile, replace platform-specific implementations with portable usage of the Chrono API.

Standard Chrono API is good from a flexibility and genericity standpoint, but terrible from an ergonomics one. The issue lies not only in the number of typedefs required to make code readable, but also the fact that no canonical duration type exists, but merely a duration template. This means a lot of APIs need to either pick one specializion or be unnecessarily generic. This favors rare, academic use cases (femtoseconds resolution) over everyday programming. Unlike C++, several programming languages with basic generics still chose single type: Java, C#, Rust.

Compare this:

auto tick = std::chrono::high_resolution_clock::now();
auto tock = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> elapsed = tock - tick;   // type inference not possible
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);

to this:

sf::Clock clock;
sf::Time elapsed = clock.getElapsedTime();
sf::Int32 millis = elapsed.asMilliseconds();
// short even without a single type inference

For sf::Time, we could provide an mplicit constructor from chrono::duration<T> and a method toDuration<T>(). The constructor being implicit would allow code based on Chrono literals (with using namespace std::chrono_literals):

sf::Time cooldown = 10s;

Filesystem API

Scope: Support the use of std::filesystem types in all APIs that load/save to files (mainly resources).

Path: Accept std::filesystem::path parameters for functions interacting with the filesystem. For easy of use, it probably makes sense to still provide overloads for different string types.

NonCopyable class

Forum thread

Scope: This class could be removed from SFML, as its functionality can be achieved more idiomatically by annotating copy member functions with = delete.

Scoped enumerations

Occurrences of unscoped enumerations (enum) should generally be replaced with scoped enumerations. We would use the enum struct variant, not enum class, as enums come much closer to a "bundle of data" than "object-oriented entity with methods".

To be discussed whether some instances benefit from their current scope being in a related class (sf::Event::KeyPressed) or if the connection to its type should be made explicit (sf::EventType::KeyPressed).

Examples:

  • sf::Keyboard::Key
  • sf::Keyboard::Scancode

Attributes

C++11/14/17 have introduced multiple attributes which can help the developer, static analysis tools and the compiler to show clear intent of the written code.

  • [[noreturn]] (C++11)
  • [[carries_dependency]] (C++11)
  • [[deprecated]] (C++14) / [[deprecated("reason")]] (C++14)
  • [[fallthrough]] (C++17)
  • [[nodiscard]] (C++17) / [[nodiscard("reason")]] (C++20)
  • [[maybe_unused]] (C++17)

Useful library types

There are various additions to the standard library which are extremely useful and should be promoted over inferior or handcrafted approaches, where possible. These may concern rather SFML implementation than public interface, and thus have low priority, but it's possible that some types appear in APIs as well.

  • std::unordered_map & Co. (supersedes std::map, see #1754)
  • std::unique_ptr (supersedes std::auto_ptr and manual memory management)
  • std::byte (supersedes void*, signed char/unsigned char/char for byte manipulation)
  • std::array (supersedes raw arrays)

There are also features which are great from a type-safety point of view, but potentially verbose in their usage. If we use them, we would need to establish idioms and best practices.

  • std::optional (possibly supersedes bool-and-output-parameter idiom)
  • std::variant (possibly supersedes unions, however quite tedious to use)

Out of scope

While newer standards offer a huge number of new features, some of them are controversial and can sometimes lead to increased code complexity. Examples are uniform initialization, perfect forwarding, string_view and quite a few more. For SFML, we would like to employ the language and library features which make user's lives easier, not put an extra burden of understanding on them or distract them with features that have little to no benefit.

SFML is not planning to use the following features (non-exhaustive list):

  • excessive noexcept (it may be appropriate for swap/move operations, but not every possible method)
  • excessive constexpr (things that are not used as constant expressions, see #1741)
  • return type deduction (increases compile times and makes APIs harder to understand; e.g. Rust decided against this feature)
  • trailing return types (alternate syntax that doesn't solve any problem that SFML has (return type depends on parameter types))
  • std::u32string or other string types in place of sf::String. Their API is still heavily influenced by the 1990's early string classes and provides basically no Unicode support.

Implementation-focused

The following features are probably irrelevant for users of the library, but should be discussed for the SFML development. They can be integrated once first SFML 3 versions have been released and therefore have lower priority.

  • type aliases (using everywhere, or just for templates?)
  • std::function and lambda expressions
  • uniform initialization -- it's actually the opposite of uniform and needs clear rules to be managed
  • override and final
  • type inference -- auto and decltype should only be used where they considerably increase readability (e.g. iterators), lack of types for basic variables can make code harder to understand
Clone this wiki locally