Prevent passing values to function/constructors in a wrong order in C++

Viewed 337

I am learning C++ and my first language in JavaScript. A common pattern to prevent bugs when passing lots of parameters to a function is to pass an object instead. The benefit of it that order doesn't matter and values are always read as intended.

Example:

function checkStuff({param1, param3, param2}) {}

checkStuff({param2, param3, param1})

I find myself constantly mixing order of parameters when passing them to class constructors. Of course, compiler and IDE will scream when types are not matching - the problem starts when my constructor definition looks like this:

 Sprite(const std::string &textureFile, int animationSpeed, int frameWidth,
     int frameHeight, int width, int height, int scale)

Is there a pattern or another option I can pass to cmake on top of this add_compile_options(-Wall -Wextra -Wpedantic) to prevent mixing these up?

2 Answers

More structure is one way to tackle the problem and one friendly with the language. You could combine things like animation speed, frame width, frame height, etc, into a single structure. Maybe ideally the language would be less awkward about this but I do find it useful in, general, to not be tempted to have like 12+ parameters we want to pass into a single function.

Also in my opinion, struct is fine... or class with all public fields (just stylistic in C++) when all you are using the object for is transmitting multiple parameter fields. Sometimes I encounter an allergy to this in C++ (like people wanting to turn private implementation tree nodes into fancy classes with separate public/private members, when they could just be a basic C-style struct) and I don't think there should be if you're just transmitting data in C++.

You know, public vs. private. The whole point of distinguishing "public" is to protect a good chunk of your code from unpredictable use cases, to maintain invariants. You defend your "kingdom". You don't have to turn everything into a "kingdom" (don't need to arm peasants in plate mail armor). Just use C-like structs to help implement "private' implementation details if you need to do so. Use the strong "shield/pavise" of the C++ knight for the most public, widely-accessible things you design. Just like with your own private parts. I mean I don't want to stand in a train with my crotch exposed, but I need to take a shower every now and then which will require me to be naked.

I generally use strong-types-for-strong-interfaces approach to prevent wrong ordering and also state the intention more clearly. It could be found a bit verbose but it makes the code more readable and less error-prone when used appropriately. You can read the article for further and detailed explanation.

I am just sharing a simple demonstration how to represent the order and meaning of the constructor parameters explicitly for the Sprite class.

#include <string>

template <typename T, typename param>
class strong_type
{
public:
     explicit strong_type ( T const& value ) : m_value ( value ) {}
     explicit strong_type ( T&& value = T{} ) : m_value ( std::move ( value ) ) {}
     T& get()
     {
          return m_value;
     }
     T const& get() const
     {
          return m_value;
     }
private:
     T m_value;
};

using animation_speed = strong_type<int , struct AnimationSpeedTag>;
using frame_width = strong_type<int , struct FrameWidthTag>;
using frame_height = strong_type<int , struct FrameHeightTag>;
using width = strong_type<int , struct WidthTag>;
using height = strong_type<int , struct HeightTag>;
using scale = strong_type<int , struct ScaleTag>;

class Sprite
{
    public:

    Sprite( const std::string &texture_file , animation_speed s , frame_width fw ,
        frame_height fh , width w , height h , scale sc ) 
        :   m_texture_file { texture_file }
        ,   m_animation_speed { s.get() }
        ,   m_frame_width { fw.get() }
        ,   m_frame_height { fh.get() }
        ,   m_width { w.get() }
        ,   m_height { h.get() }
        ,   m_scale { sc.get() }
        {   }

    private:

        // There is no harm to hold the values as primitive types
        // Just take it as `strong type`
        std::string m_texture_file;
        int m_animation_speed {};
        int m_frame_width {};
        int m_frame_height {};
        int m_width {};
        int m_height {};
        int m_scale {};
};

int main() {

    // instead of
    // Sprite my_sprite { "sprites/char.dds" , 12 , 640 , 480 , 320 , 240 , 1 };

    Sprite my_sprite { "sprites/char.dds" , animation_speed { 12 } , 
                    frame_width { 640 } , frame_height { 480 } ,
                    width { 320 } , height { 240 } , scale { 1 } };
}

run online

Another way is passing a struct which holds the required parameters, but I don't suggest that way. Because actually it is not solving the problem, just moving it one step further. The parameters in struct still might have been left unassigned.

A demonstration :

#include <string>

struct sprite_parameters
{
    std::string texture_file;
    int animation_speed {};
    int frame_width {};
    int frame_height {};
    int width {};
    int height {};
    int scale {};
};

class Sprite
{
    public:

    Sprite( const sprite_parameters& params ) 
        :   m_texture_file { params.texture_file }
        ,   m_animation_speed { params.animation_speed }
        ,   m_frame_width { params.frame_width }
        ,   m_frame_height { params.frame_height }
        ,   m_width { params.width }
        ,   m_height { params.height }
        ,   m_scale { params.scale }
        {   }

    private:

        std::string m_texture_file;
        int m_animation_speed {};
        int m_frame_width {};
        int m_frame_height {};
        int m_width {};
        int m_height {};
        int m_scale {};
};

int main() {

    // instead of
    // Sprite my_sprite { "sprites/char.dds" , 12 , 640 , 480 , 320 , 240 , 1 };
    sprite_parameters params;
    params.texture_file = "sprites/char.dds";
    params.animation_speed = 150;
    params.frame_width = 320;
    params.frame_height = 240;
    params.width = 640;
    params.height = 480;
    // params.scale = 1; OK, I forgot this, what will happen now. 
    // It is just ugly obscure bug.
    Sprite my_sprite { params };
}

run online

Related