🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

C++ tutorials

Started by
93 comments, last by taby 1 year, 9 months ago

I think a better example of encapsulation principles would be a normalized vector. There the design contract is that the vector's length must be exactly 1, and you would want to prevent someone from coming in and directly modifying the components to make it not be 1 anymore.

// this is only a 2-vec and you probably want a 3-vec, but for illustrative purposes...
struct Vector {
  float x;
  float y;
};

struct NormalizedVector
{
private:
  float x_;
  float y_;
public:
  explicit NormalizedVector(const Vector& vec) {
    const float length = sqrtf(vec.x * vec.x + vec.y * vec.y);
    assert(length);
    x_ = vec.x / length;
    y_ = vec.y / length;
  }
  float x() const { return x_; }
  float y() const { return y_; }
};

In this case, however, you wouldn't want to have individual setters for the components, because setting individual components would make it no longer normalized; it's not really a meaningful operation. You still might allow adding to or scaling a NormalizedVector, however, but then because of the contract (always length 1) you would return a regular vector (one with no restrictions enforced on it at the type level) from those methods.

Vector operator*(float scalar, const NormalizedVector& vec) {
  return Vector { scalar * vec.x(), scalar * vec.y() };
}

Or perhaps a rotation operation…

NormalizedVector NormalizedVector::Rotate(float angle) {
  // ...
}

In general, for objects that aren't simply data, that actually enforce a design contract, you want most mutators to be verbs, not simple setters. Even if your setters transform or validate their input somehow, calling them “SetFoo” is pretty weak encapsulation, since conceptually the method is still reaching inside the object to set a specific field on it, rather than carrying out some sort of meaningful operation that one can do with the type. If a class's mutators are mostly weak encapsulated “SetFoo” type methods, then this is where I'd start to question whether there's much in the way of a meaningful design contract here to begin with.

Advertisement

Now that’s the spirit. I will work on it tonight!

Oh yes, the std::complex<> class makes the x and y variables private or protected. Why, I don't know. There is overloaded real() and imag() functions, one pair to read the values, and another pair to write the values. So rather than calling them getters and setters, they used the same function name. What is the reasoning behind this, if it's so awful? Surely, Stroustrup is correct?

https://github.com/sjhalayka/basic_fractals/blob/master/2d.cpp

taby said:

Oh yes, the std::complex<> class makes the x and y variables private or protected. Why, I don't know. There is overloaded real() and imag() functions, one pair to read the values, and another pair to write the values. So rather than calling them getters and setters, they used the same function name. What is the reasoning behind this, if it's so awful? Surely, Stroustrup is correct?

https://github.com/sjhalayka/basic_fractals/blob/master/2d.cpp

First of all, let's clear up some misconceptions. Firstly, Stroustrup is not infallible. He's a human being like all of us. In any case, C++'s standard library was not designed by Stroustrup alone - the standard in general is helmed by a committee rather than a Python-style “Benevolent Dictator for Life." In theory anyone can propose an addition to the standard and get it accepted to the language. In practice, only people with the time and energy to go through the process and all the bikeshedding involved with it will ever get something into the standard. I'm told it's exhausting and many a fine interface has not been accepted and had to undergo some sort of change that broke its cleanliness. The committee has in fact made mistakes in the past. The one that comes to mind immediately is omitting an equivalent of make_shared from C++11 (one was added in C++14), which a lot people seem to agree was pretty silly. Not all of us agree with their decisions that aren't obvious mistakes, either.

The more salient point, though, becomes evident when we take a look at the section on “Implementation notes” on the cppreference page for std::complex: https://en.cppreference.com/w/cpp/numeric/complex​ It seems the committee wanted (for reasons not entirely clear to me) to decouple the way the fields are stored from the interface to allow standard library implementors to choose what the underlying storage is. That's a valid reason for having accessors like this. Most of the time the classes you write will not have this requirement, especially at the beginner level.

For comparison, take a look at std::pair (and the similar if more heavyweight std::tuple): https://en.cppreference.com/w/cpp/utility/pair​ The design contract of pair imposes no requirements on the values the elements of the pair make take and the desired interface is identical to what we would have for a simple structure with two public fields, so that's the form it takes. In general, you'd want to guard access to a field when the type's design contract specifies that not all possible values of that field's type are allowable.

Thank you for sharing your insights and feelings.

Oberon_Command said:

taby said:

Oh yes, the std::complex<> class makes the x and y variables private or protected. Why, I don't know. There is overloaded real() and imag() functions, one pair to read the values, and another pair to write the values. So rather than calling them getters and setters, they used the same function name. What is the reasoning behind this, if it's so awful? Surely, Stroustrup is correct?

https://github.com/sjhalayka/basic_fractals/blob/master/2d.cpp

First of all, let's clear up some misconceptions. Firstly, Stroustrup is not infallible. He's a human being like all of us. In any case, C++'s standard library was not designed by Stroustrup alone - the standard in general is helmed by a committee rather than a Python-style “Benevolent Dictator for Life." In theory anyone can propose an addition to the standard and get it accepted to the language. In practice, only people with the time and energy to go through the process and all the bikeshedding involved with it will ever get something into the standard. I'm told it's exhausting and many a fine interface has not been accepted and had to undergo some sort of change that broke its cleanliness. The committee has in fact made mistakes in the past. The one that comes to mind immediately is omitting an equivalent of make_shared from C++11 (one was added in C++14), which a lot people seem to agree was pretty silly. Not all of us agree with their decisions that aren't obvious mistakes, either.

The more salient point, though, becomes evident when we take a look at the section on “Implementation notes” on the cppreference page for std::complex: https://en.cppreference.com/w/cpp/numeric/complex​ It seems the committee wanted (for reasons not entirely clear to me) to decouple the way the fields are stored from the interface to allow standard library implementors to choose what the underlying storage is. That's a valid reason for having accessors like this. Most of the time the classes you write will not have this requirement, especially at the beginner level.

For comparison, take a look at std::pair (and the similar if more heavyweight std::tuple): https://en.cppreference.com/w/cpp/utility/pair​ The design contract of pair imposes no requirements on the values the elements of the pair make take and the desired interface is identical to what we would have for a simple structure with two public fields, so that's the form it takes. In general, you'd want to guard access to a field when the type's design contract specifies that not all possible values of that field's type are allowable.

The C++ standard is nothing more than a specification that outlines the syntax, pre- and post conditions, memory requirements and perfromance characteristics that the implementations should adhere too. How you implement this is up to the library or compiler implementer which leaves freedom in how to achieve these requirements. It also means that hard to sovle issues can be classed as undefined behaviour and you are at the mercy of the implmenter at this point. For example union:

union Test
{
	float m_1;
	int m_2;
};

void useUnion(Test test)
{
   test.m_1 = .01f;
   int value = test.m_2; //This is undefined behavior, because there could be really nasty conversion operators involved here if this would not be undefined behaviour.
}

I am using trivial types here in the union but imagine there were complicated user defined types in that union instead, that issue is much much harder and not something a specification can outline even.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

Etes-vous Canadien?

I live in Canada but, I am not Canadian. Why do you want to know?

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

I saw that you worked on Watch Dogs, so I wondered if you were Canadian. I have an office at the University of Saskatchewan in Saskatoon, but I work remotely a lot.

Ha, ha, yet another C++ war. Just use it however it suits you.

This topic is closed to new replies.

Advertisement