Explicit function parameters in C++ 11 (and a lesson in cross platform data types...)
A slight detour from the normal content this post, but I recently discovered a neat trick in C++ which I'd really like to share. When working on xygine recently I blundered into a bug when using the following function in the MultiRenderTexture class:
create(sf::Uint32 width, sf::Uint32 height, std::size_t count, bool depthBuffer = false);
What this particular function actually does isn't important, so much as what I was doing wrong. (If you're interested it creates a new render target sized width * height with count textures). In my code I mistakingly missed out the count parameter:
create(w, h, true);
The problem with this is that the boolean value implicitly converts to an integer without even a warning, and because of the default value supplied in the signature of the function what the compiler saw was:
create(w, h, 1, false);
The bug right here was that I couldn't fathom why a depth buffer wasn't being created on my render target (due to the 'false' parameter). Once I finally figured out what exactly the bug was, it got me thinking about how class constructors can be labelled 'explicit' to prevent this sort of thing from happening, and how it'd be useful to do the same for normal (and member) functions. The explicit keyword cannot be applied to functions in the same way as constructors, but a short trip on the googlemobile revealed this, in my opinion, much underrated answer on stackoverflow. The idea is this: using the C++11 delete feature (you've probably seen it applied to default copy constructors and assignment operators) which *can* be applied to functions, you can create a deleted template function so that all but the explicitly stated overloads are deleted. These signatures are then implicitly explicit, if you will. The modified create function then looked like this:
create(sf::Uint32 width, sf::Uint32 height, std::size_t count, bool depthBuffer = false);
template <typename T>
create(sf::Uint32 width, sf::Uint32 height, T count, bool depthBuffer = false) = delete;
Now with anything but a size_t as the count value the compiler would throw an error. Perfect. Or was it? What followed next was a little lesson in typedefs, and cross platform implementations of the STL.
Eagerly I tried my modified source on linux. On windows I had been working with a 32-bit build, and with the explicit function parameters in place I needed to state '1u' as the count value. Not even '1' but '1u' as std:size_t is a typedef for an unsigned int, and the signature had to match exactly. Unfortunately when compiling the source on xubuntu with g++5.1 I was presented with an error claiming no matching signature was found. Hm.
Being the patient and open minded soul that I am I immediately wrote it off as 'a bug in g++', cursed linux a little bit, then went away disappointed my new found trick wasn't going to work everywhere. Of course I was wrong. Later that day a discussion on IRC about how std::time_t was a 64bit value in 64bit builds but only 32 bits in size on 32bit builds got me thinking... to which other typedefs does this apply? Quickly I realised that, unlike my current windows build, my linux build was 64bit and, indeed, std::size_t was a 64bit typedef. To get a matching signature g++ was expecting '1ul' not '1u'. Hoist by my own explicit petard! Simply changing the type from std::size_t to sf::Uint32 (SFML has its own set of nice cross-platform typedefs, we'll ignore the fact that any of these types are hugely overkill when representing any value less than 5...) ensured that the deleted template method now worked across all builds. Fantastic!
And not one, but two lessons learned.
create(sf::Uint32 width, sf::Uint32 height, std::size_t count, bool depthBuffer = false);
What this particular function actually does isn't important, so much as what I was doing wrong. (If you're interested it creates a new render target sized width * height with count textures). In my code I mistakingly missed out the count parameter:
create(w, h, true);
The problem with this is that the boolean value implicitly converts to an integer without even a warning, and because of the default value supplied in the signature of the function what the compiler saw was:
create(w, h, 1, false);
The bug right here was that I couldn't fathom why a depth buffer wasn't being created on my render target (due to the 'false' parameter). Once I finally figured out what exactly the bug was, it got me thinking about how class constructors can be labelled 'explicit' to prevent this sort of thing from happening, and how it'd be useful to do the same for normal (and member) functions. The explicit keyword cannot be applied to functions in the same way as constructors, but a short trip on the googlemobile revealed this, in my opinion, much underrated answer on stackoverflow. The idea is this: using the C++11 delete feature (you've probably seen it applied to default copy constructors and assignment operators) which *can* be applied to functions, you can create a deleted template function so that all but the explicitly stated overloads are deleted. These signatures are then implicitly explicit, if you will. The modified create function then looked like this:
create(sf::Uint32 width, sf::Uint32 height, std::size_t count, bool depthBuffer = false);
template <typename T>
create(sf::Uint32 width, sf::Uint32 height, T count, bool depthBuffer = false) = delete;
Now with anything but a size_t as the count value the compiler would throw an error. Perfect. Or was it? What followed next was a little lesson in typedefs, and cross platform implementations of the STL.
Eagerly I tried my modified source on linux. On windows I had been working with a 32-bit build, and with the explicit function parameters in place I needed to state '1u' as the count value. Not even '1' but '1u' as std:size_t is a typedef for an unsigned int, and the signature had to match exactly. Unfortunately when compiling the source on xubuntu with g++5.1 I was presented with an error claiming no matching signature was found. Hm.
Being the patient and open minded soul that I am I immediately wrote it off as 'a bug in g++', cursed linux a little bit, then went away disappointed my new found trick wasn't going to work everywhere. Of course I was wrong. Later that day a discussion on IRC about how std::time_t was a 64bit value in 64bit builds but only 32 bits in size on 32bit builds got me thinking... to which other typedefs does this apply? Quickly I realised that, unlike my current windows build, my linux build was 64bit and, indeed, std::size_t was a 64bit typedef. To get a matching signature g++ was expecting '1ul' not '1u'. Hoist by my own explicit petard! Simply changing the type from std::size_t to sf::Uint32 (SFML has its own set of nice cross-platform typedefs, we'll ignore the fact that any of these types are hugely overkill when representing any value less than 5...) ensured that the deleted template method now worked across all builds. Fantastic!
And not one, but two lessons learned.
Comments
Post a Comment