-
Notifications
You must be signed in to change notification settings - Fork 1.7k
SD: CPP 17 Support
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.
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.
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.
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
- The new standard can be platform and toolchain independently requested with
set(CMAKE_CXX_STANDARD 17)
- There shouldn't be any additional changes required
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.
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.
Scope: Remove sf::Thread
, sf::Mutex
, sf::Lock
, sf::ThreadLocal
, sf::ThreadLocalPtr
and replace them with their standard counterparts.
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;
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.
Scope: This class could be removed from SFML, as its functionality can be achieved more idiomatically by annotating copy member functions with = delete
.
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
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)
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. (supersedesstd::map
, see #1754) -
std::unique_ptr
(supersedesstd::auto_ptr
and manual memory management) -
std::byte
(supersedesvoid*
,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)
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 ofsf::String
. Their API is still heavily influenced by the 1990's early string classes and provides basically no Unicode support.
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
andfinal
- type inference --
auto
anddecltype
should only be used where they considerably increase readability (e.g. iterators), lack of types for basic variables can make code harder to understand