Is this how you do object chaining?

Im learning about object chaining and im wondering if im doing it correctly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <print>
#include <string>
#include <vector>

class Map
{
    public:
        Map(const std::string& name, int height, int width, char tile) 
            : mName(name), mHeight(height), mWidth(width), mTile(tile)
        { }

        Map& Initialize()
        {
            mMap.assign(mHeight, std::vector<char>(mWidth, mTile));
            isInitialized = true;

            return *this;
        }

        Map& Draw()
        {
            if(isInitialized == true)
            {
                for (size_t column = 0; column < mHeight; ++column)
                {
                    for (size_t row = 0; row < mWidth; ++row)
                    {
                        std::print("{} ", mMap[column][row]);
                    }
                    std::print("\n");
                }
            }
            else
            {
                std::print("ERROR: Map is not initialized!");
            }

            return *this;
        }

    private:
        std::string mName{};
        int mHeight{};
        int mWidth{};
        char mTile{};
        bool isInitialized{};

        std::vector<std::vector<char>> mMap;
};

int main()
{
    Map test("Test", 10, 10, '.');
    test.Initialize().Draw();
}
Yep! This is one among a few ways this technique is implemented in C++, you simply return a reference to the instance of the class (*this). Method chaining is also employed with the operator<<, where you return an ostream& so that you can keep using the << operator to put stuff on the stream.

It's part of a broader concept called "Fluent API" [1] /"Fluent Interface" [2]. I heard about it from some web dev video a few years ago (apparently it's common in javascript).

I use this idiom in my classes when I define class methods that are meant to be used to initialize the instance.

[1] https://en.wikipedia.org/wiki/Fluent_interface
[2] https://stackoverflow.com/a/77122397/3396951
Last edited on
Just a few unsolicited comments on your code:
- Why not have Initialize() be part of the constructor? The point of constructors is make the initial state of your object viable without extra external steps. You can't always do this, but in this case I think you can.
- Draw() does not mutate the state of the class, so it can be marked "const". (this might technically be true, but I realize it makes it hard to do the "chaining" which was the point of this thread, so nevermind)
- You use int for mHeight/mWidth, but you use size_t for column/row. This should be more consistent.
- Your inner loop increments "row" but you expand horizontally in the inner loop, suggesting a change in column. This seems misleading to me. I think you're accidentally transposing? Or the variables are just misnamed, I'm not sure which.

But you are correct to increment the outer array with the outer loop variable, for cache performance.
Last edited on
Is this better?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <print>
#include <string>
#include <vector>

class Map
{
    public:
        Map(const std::string& name, int height, int width, char tile) 
            : mName(name), mHeight(height), mWidth(width), mTile(tile)
        {
            mMap.assign(mHeight, std::vector<char>(mWidth, mTile));
        }

        void Draw()
        {
            for (size_t column = 0; column < mHeight; ++column)
            {
                for (size_t tile = 0; tile < mWidth; ++tile)
                {
                    std::print("{} ", mMap[column][tile]);
                }
                std::print("\n");
            }
        }

    private:
        std::string mName{};
        size_t mHeight{};
        size_t mWidth{};
        char mTile{};
        bool isInitialized{};

        std::vector<std::vector<char>> mMap;
};

int main()
{
    Map test("Test", 10, 10, '.');
    test.Draw();
}
Yeah that's better, imo.
I still think it's a bit strange that you print a newline every time you increment "column", since each newline would be a new row, not column. It seems like "column" should be renamed to "row" but maybe I'm just being nitpicky.
I sometimes split out the initialize and constructor, because sometimes I want to reset an object back to its default state or reconstruct it. I know there are other ways to do it, but I tend to keep it simple in a shared function that is used by the ctor but also available to the user for that purpose. So there can be reasons for doing that one.
Agreed, that's perfectly acceptable, too, and is what I actually had in mind. I just personally wouldn't make Initialize() be a public function in that case. I would imagine it being more like std::vector::clear(), which then might internally call vector::initialize() in our hypothetical.

(But now I'm once again getting caught up on naming and not the overall design, oops!)
Last edited on
There's no need to initialise a std::string with {}. That is the default.

The constructor params are of type int but these are then stored in type size_t. These should be of type size_t in the constructor.

size_t column = 0 is just size_t column {}

Rather than passing name by ref, you can pass by value and use a std::move

Map(std::string name, size_t height, size_t width, char tile)
: mName(std::move(name)), mHeight(height), mWidth(width), mTile(tile)

See https://www.cppstories.com/2018/08/init-string-member/

The for loops can be 'reduced' (although some reading this won't like it)

1
2
for (size_t column {}; column < mHeight; ++column, std::print ("\n"))
    for (size_t tile {}; tile < mWidth; std::print("{} ", mMap[column][tile++]));

1
2
3
4
5
6
Map(const std::string& name, int height, int width, char tile) 
: mName(name), mHeight(height),
  mWidth(width), mTile(tile)
{
    mMap.assign(mHeight, std::vector<char>(mWidth, mTile));
}

Constructs mMap and then changes its value in the body.

This initializes mMap with desired value already on construction:
1
2
3
4
5
6
Map(const std::string& name, int height, int width, char tile) 
: mName(name), mHeight(height),
  mWidth(width), mTile(tile),
  mMap(mHeight, std::vector<char>(mWidth, mTile))
{
}

Thank you for all the replies! That seems like a straight forward concept!
Registered users can post here. Sign in or register to post.