Get/Set Methods In OOD

Started by
26 comments, last by Dae 15 years, 2 months ago
Quote:Original post by Sneftel
Quote:Original post by phresnel
Fear not, I don't do so (and actually never have done). I was answering on Telastyns example

That still doesn't answer my question. The code you posted doesn't allow property getter/setters to access the encompassing class, making them essentially useless as far as doing anything you'd actually want to do with properties (other than print a debugging message, I suppose).


I see. In that case, you can still cherry pick what you pipe into the properties, let it be single primitive types or the whole parent class, let them be mutable or const (see class Foo::Delta):

struct Foo {    // Properties using unnamed classes.    class Alpha {            int value;        public:            int & operator = (const int &i) { return value = i; }            operator int () const { return value; }    } alpha;    // Cherry picking.    class Bravo {            float value;            Alpha & alpha;        public:            float & operator = (const float &f) { alpha = 0xdeadbeef; return value = f; }            operator float () const { return value; }            Bravo (Alpha &#945;) : alpha (alpha) {};    } bravo;    // Alias for bravo. No cherry picking this time.    class Charlie {            Foo & foo;        public:            float & operator = (const float &f) { return foo.bravo = f; }            operator float () const { return foo.bravo; }            Charlie (Foo &foo) : foo (foo) {};    } charlie;    // Readonly alias for bravo. No cherry picking this time.    /*    class Delta {            const Foo & foo;        public:            float & operator = (const float &f) { return foo.bravo = f; } // < compile time error, as foo is immutable            operator float () const { return foo.bravo; }            Delta (const Foo &foo) : foo (foo) {};    } delta;    */    Foo () : bravo (alpha), charlie (*this), delta (*this) {}};int main () {    Foo foo;    foo.charlie = 5.132f;    ::std::cout << ::std::hex << foo.alpha << ", "                << foo.bravo << ", "                << ::std::endl;}


Allowedly, Foo will grow and grow in bytesize. So as for the size, clearly an advantage for c#. But as for the degree of freedom w.r.t. security levels, I'd say advantage for c++. But all in all, I am unsure which properties and property-hacks are better. C++ is good if you are paranoid, and make everything const- and restricted-first. But c#'s syntax is clearly the easier and more compact, plus it doesn't need additional storage.

To give a more realistic example:

#include <iostream>class Angle {    float value;    class Radian {        float &value;    public:        float & operator = (const float &f) { return value = f; }        operator float () const { return value; }        Radian (float &value) : value (value) {}    };    class Degree {        float &value;    public:        float & operator = (const float &f) { return value = f * 0.0174532925f; }        operator float () const { return value * 57.2957796f; }        Degree (float &value) : value (value) {}    };public:    Radian radian;    Degree degree;    Angle () : radian (value), degree (value) {}};int main () {    Angle alpha;    alpha.degree = 180.0f;    ::std::cout << alpha.degree << " degrees = " << alpha.radian << " radians\n";    alpha.radian = 3.1415927f * 0.5f;    ::std::cout << sizeof (alpha) << "]" << alpha.degree << " degrees = " << alpha.radian << " radians\n";}


or just (main function skipped)

class Angle {    class Degree {        float &value;    public:        float & operator = (const float &f) { return value = f * 0.0174532925f; }        operator float () const { return value * 57.2957796f; }        Degree (float &value) : value (value) {}    };public:    float radian;    Degree degree;    Angle () : degree (radian) {}};


compared to

class Angle {    float value;    public float Radian {        get { return this.value; }        set { this.value = value; }    }        public float Degree {        get { return this.value * 57.2957796f; }        set { this.value = value * 0.0174532925f; }    }};namespace ConsoleApplication1 {    class Program {        static void Main (string [] args) {            Angle alpha = new Angle ();            alpha.Degree = 180.0f;            System.Console.WriteLine ("{0} degrees = {1} radians", alpha.Degree, alpha.Radian);            alpha.Radian = 3.1415927f * 0.5f;            System.Console.WriteLine ("{0} degrees = {1} radians", alpha.Degree, alpha.Radian);        }    }}


or just (main function skipped)

class Angle {    public float Radian;    public float Degree {        get { return Radian * 57.2957796f; }        set { Radian = value * 0.0174532925f; }    }}


.




Quote:Original post by mikeman
Sure, writing get/set for each and every one of your private data is sign of bad design, but some properties are meant to be public. Like the color of a Renderable. You could name it SetColor(),ReColor(),RePaint() or what have you, but in essense it's a setter. I think there's a line between encapsulating and making your objects xenophobic.


There the divergence already begins, as in my world the things I render can have a different color at each spacetime-point on it's surface (yes, it's about a ray tracer), making it impossible to supply a virtual SetColor method for all of them (just imagine spheres, cubes, player-models, or whole terrain including flora).



edit: But one thing I dislike about properties in general: They look like peas when used, but can really be coconuts. I like the convention where you name your functions like convertToYYY() or computeZZZ(), when some work is done internally, so fellow programmers don't underestimate a line of code in performance when in eye parse mode. And for the same reason, I virtually never use get/set-calls, because it makes sense only in the rarest conditions (that is, in my code).


Also, I really wouldn't use the above Degree/Radian-Angle class, as it is hard to analyze which form is used more often in general (and based on that knowledge to decide which form is first class), but the wrong decision would introduce a plethora of unneccessary multiplications. So I would instead provide two distinct classes (again using the radian/degree case, but you could also apply this to other things, e.g. normal vectors / general vectors / points):

// NOTE: if you detect a "&deg;" somewhere in the following,//       please replace it with an actual "&" and a following "deg"namespace {    const float to_degree = 57.2957796f;    const float to_radian = 0.0174532925f;}class Degree;class Radian {        float value;    public:        Radian (const float &init) : value (init) {}        explicit inline Radian (const Degree &);        inline operator Degree () const ;        inline Radian & operator = (const Degree &rad);        operator float () const { return value; }        float & operator = (const float &f) { return value = f; }};class Degree {        float value;    public:        Degree (const float &init) : value (init) {}        explicit inline Degree (const Radian &rad) ;        inline operator Radian () const ;        inline Degree & operator = (const Radian &rad);        operator float () const { return value; }        float & operator = (const float &f) { return value = f; }};inline Radian::Radian (const Degree & deg) : value (to_radian * deg) {}inline Radian & Radian::operator = (const Degree & deg) { value = to_radian * deg; return *this; }inline Radian::operator Degree () const { return Degree (value * to_degree); }inline Degree::Degree (const Radian &rad) : value (to_degree * rad) {}inline Degree & Degree::operator = (const Radian &rad) { value = to_degree * rad; return *this; }inline Degree::operator Radian () const { return Radian (value * to_radian); }


This erases hidden computations, and introduces additional ones only on demand:

int main () {    Degree deg = 90.0f;    Radian radA (deg);    Radian radB = deg;    ::std::cout << deg << " degrees = " << radA << " radians = " << radA << " radians\n";    Degree degB = 45.0f + deg + 45.0f + (Degree)radA;    ::std::cout << "degB = " << degB << "\n";}


[Edited by - phresnel on January 26, 2009 8:23:40 AM]

Advertisement
That's all well and good, phresnel, but those implementations of properties in C++ will only allow basic types (int, float, bool, etc). Meaning you can't store a property of a class, and access the class members/methods, at least without forcing operator -> and pointers. Versus dot syntax and references with get/set functions. :-/
010001000110000101100101
Quote:Original post by Dae
That's all well and good, phresnel, but those implementations of properties in C++ will only allow basic types (int, float, bool, etc). Meaning you can't store a property of a class, and access the class members/methods, at least without forcing operator -> and pointers. Versus dot syntax and references with get/set functions. :-/


Veto:


#include <iostream>enum { debug = 0 };class UberFloat {    float value;public:    float & operator = (const float &f) {        if (debug) { ::std::cout << "UberFloat::operator = (" << f << ")" << ::std::endl; }        return value = f;    }    float & operator = (const UberFloat & f) {        if (debug) { ::std::cout << "UberFloat::operator = (" << f << ")" << ::std::endl; }        return value = f.value;    }    operator float () const {        if (debug) { ::std::cout << "UberFloat::operator float () (" << value << ")" << ::std::endl; }        return value;    }    UberFloat (float value = 0.0f) : value (value) {}    friend const UberFloat operator + (const UberFloat &lhs, const UberFloat &rhs);    friend const UberFloat operator + (const float lhs, const UberFloat &rhs);    friend const UberFloat operator + (const UberFloat &lhs, const float rhs);    friend std::ostream& operator<< (std::ostream& o, const UberFloat& uber);};const UberFloat operator + (const UberFloat &lhs, const UberFloat &rhs) {    return UberFloat (lhs.value + rhs.value);}const UberFloat operator + (const float lhs, const UberFloat &rhs) {    return UberFloat (lhs + rhs.value);}const UberFloat operator + (const UberFloat &lhs, const float rhs) {    return UberFloat (lhs.value + rhs);}std::ostream& operator<< (std::ostream& o, const UberFloat& uber) {   return o << uber.value << "uf";}
UberFloat, an imaginary class type.


template <typename T> class Angle {    T value;    class Radian {        T &value;    public:        T & operator = (const T &f) { value = f; return value; }        operator const T & () const { return value; } // Give back reference to T.        Radian (T &value) : value (value) {}    };    class Degree {        T &value;    public:        T & operator = (const T &f) { value = f * 0.0174532925f; return value; }        operator float () const { return value * 57.2957796f; } // Give back a float.        Degree (T &value) : value (value) {}    };public:    Radian radian;    Degree degree;    Angle () : radian (value), degree (value) {}};
Angle<>, (note how Angle<T>::Radian::operator const T& returns a reference of type T, while Angle<T>::Degree::operator float gives back a float; this is just for demonstration purposes)

template <typename T> void test () {    Angle<T> alpha;    alpha.degree = 180.0f;    ::std::cout << alpha.degree << " degrees = "                << alpha.radian << " radians\n";    alpha.radian = 3.1415927f * 0.5f;    alpha.radian = alpha.radian + 3.1415927f;    ::std::cout << alpha.degree << " degrees = " << alpha.radian << " radians\n";}int main () {    test <UberFloat>();    test <float>();}
A test, including a primitive type and a class type.

Output:
180 degrees = 3.14159uf radians270 degrees = 4.71239uf radians180 degrees = 3.14159 radians270 degrees = 4.71239 radians


Note how radians are printed with the "uf"-postfix in the first test. This is because Angle<UberFloat>::Radian::operator const UberFloat & () gives back a reference to UberFloat, for which we defined an operator <<, which outputs the underlying value plus the "uf"-postfix. Whereas for Angle<>::Degrees we only defined an operator float ().


Still, I really don't use such constructs. See my sooner peas/coconuts example.

[Edited by - phresnel on January 28, 2009 4:56:59 AM]
Quote:Original post by phresnelVeto:



That's nice phresnel, but not what I meant. Sorry, I meant basic syntax:

#include <iostream>class A{    public:    int roar;    A() : roar(5) { }};class B{    public:    class    {        A value;    public:        A& operator = (const A &a) { return value = a; }        operator A() const { return value; }    } property;};int main(){    B b;    std::cout << A(b.property).roar; // have to cast or use -> operator, can't use dot operator - unlike C# - ugly syntax either way    //std::cout << b.property.roar; // :-(}
010001000110000101100101
Quote:Original post by Dae
Quote:Original post by phresnelVeto:



That's nice phresnel, but not what I meant. Sorry, I meant basic syntax:

*** Source Snippet Removed ***


Now it's getting interesting [smile]

#include <iostream>struct A {    int roar;    A(int roar=5) : roar(roar) { }};struct B {


The ugly solution as you mentioned:
    // The cherry pick or ugliness solution.    class property_t {        A value;    public:        A& operator = (const A &a) { return value = a; }        operator A() const { return value; }        property_t () : roar (value.roar) {}        // Ugly operators.        A const * const operator -> () const { return &value; }        A * const operator -> () { return &value; }        // Can make it a property again.        int &roar;    } property;


The more interesting solution:
    // Allow member access, still can validate.    struct : public A {        A& operator = (const A &a) {            if (a.roar > 42)                ::std::cout << "Out of cheese error. Redo from start." << ::std::endl;            A::operator = (a);            return *this;        }        operator A() const { return *this; }    } property2;};


And the test:
int main(){    B b;    ::std::cout << A(b.property).roar << ::std::endl;    b.property->roar = 42;    ::std::cout << b.property->roar << ::std::endl;    b.property.roar = 42 * 2;    ::std::cout << b.property.roar << ::std::endl;    b.property2.roar = 42 * 4;    ::std::cout << b.property2.roar << ::std::endl;    b.property2 = A (42 * 4);    ::std::cout << b.property2.roar << ::std::endl;}


Plus the output:
54284Out of cheese error. Redo from start.168



edit: But clearly (that is, after a coffee break), "the more interesting solution" fails to inject code there:

b.property2.roar  ^ here  ^


So, the last (not serious, don't use! it will break all your code and stuff! really!) resort to enable dot-notation on c++-properties is to fall back to preprocessor romanticism:
    **** snip ****    // Allow member access, still can validate.    struct : public A {        A& operator = (const A &a) {            if (a.roar > 42)                ::std::cout << "Out of cheese error. Redo from start." << ::std::endl;            A::operator = (a);            return *this;        }        A &rebind () {            ::std::cout << "Gerbils are the better programmers." << ::std::endl;            return *this;        }    } property2;};#define property2 property2.rebind()int main(){    B b;    b.property2.roar = 42;    ::std::cout << b.property2.roar << ::std::endl;}


Output:
Gerbils are the better programmers.42


[Edited by - phresnel on January 28, 2009 6:55:37 AM]
I have a question for anyone in particular..
Is it always better design to use properties in methods INSIDE the object itself, if the properties exist? Or is that decided upon based on situation?

Also, can properties be overloaded for assignments and returns of different datatypes?
Quote:Original post by HumanoidTyphoon
I have a question for anyone in particular..
Is it always better design to use properties in methods INSIDE the object itself, if the properties exist? Or is that decided upon based on situation?

IMHO, this depends on the situation. Sometimes, for example, using properties inside the object can yield [self-] recursion. Think of the situation where a property is not intended to be a public (validated) alias for member fields.

I would say, for performance, as a rule of thumb, don't use if applicable. But for encapsulation reasons (e.g. future safety, code path safety), use them if applicable. Make your choice.


Quote:Also, can properties be overloaded for assignments and returns of different datatypes?


Not in c#, I guess.
Quote:Original post by phresnel*** Source Snippet Removed ***


Thanks for trying phresnel (rating++). I needed closure on the whole "C++ doesn't have properties" argument.

C++ properties may allow more advanced control over C#, but are not syntactically as simple to implement or use. Due to this, get/set methods seem to be the best solution.

You shouldn't have to re-implement the members/methods inside the property class as you've done in the "ugly solution". That's "uglier" than what I actually meant, even if it's possible, it won't work with templates.

It will work fine overloading the -> and = operators, sure, but now you've complicated usage, and are throwing around pointers where references should be used. This is the only solution that's practical and you see it all over the web.

I have already looked into the "interesting solution", where you inherit the destination class, and yes it does fail to check with dot notation, because you would need to overload all the underlying members. When you access "roar" it goes straight to the datatype of the "roar" variable's operator =, not the class it's within.

Nice preprocessors, the only solution has the same usage as C#. Too bad you can't give #define macros "context" (only the instances of -that- class, or only -this- namespace) so that they wouldn't screw up everything else in your program globally.

Edit: Wait, hold up! I didn't notice what you were doing there in your "interesting solution" was not exactly what I meant. Why would you overload operator = and check the variable "roar"? We don't care about the property's underlying members/methods. We just want access to them with the dot operator. That's all. That is accomplished by inheriting the destination class as you've done with the anonymous struct. Fine. Great. The problem? The operator = will check if the argument is fine, and if it is it changes the stored object to it, but since we are inheriting the class, not storing an object of the class, how do we say "change 'this' into 'a'":

#include <iostream>class A{    public:    int roar;    A() : roar(5) { }};class B{    public:    struct : public A    {        A& operator = (const A &a)        {            // if("a" is fine) then change "this" variable into "a". copy a to this, without doing it manually (ie. this.roar = a.roar)            // normally we'd just do: value = a; return value;                        return *this;        }        operator A() const { return *this; }    } property;};int main(){    B b;    b.property.roar = 10;    A a;    b.property = a;    std::cout << b.property.roar;}


[Edited by - Dae on January 28, 2009 2:19:45 PM]
010001000110000101100101

This topic is closed to new replies.

Advertisement