[Forward]C++ Properties - a Library Solution

orig url: https://accu.org/index.php/journals/255

roperties are a feature of a number of programming languages - Visual Basic and C# are two of them. While they are not part of standard C++, they have been implemented in C++ for the CLI (popularly known as .Net) environment and Borland C++ Builder, which comes with a library of useful components which make heavy use of properties in their interfaces. And when programmers are asked their ideas for extensions to C++, properties are a popular request. [Wiegley,Vandevoorde]

So what is a property? The C++/CLI draft spec says, "A property is a member that behaves as if it were a field... Properties are used to implement data hiding within the class itself." In the class declaration, properties must be declared with a special keyword so the compiler knows to add appropriate magic:

class point {
private:
  int Xor;
  int Yor;
public:
  property int X {
    int get() {
      return Xor;
    }
    void set(int value) {
      Xor = value;
    }
  }
  ...
};

point p; p.X = 25;

In application code, properties look like ordinary data members of a class; their value can be read from and assigned to with a simple = sign. But in fact they are only pseudo-data members. Any such reference implicitly invokes an accessor function call "under the covers" and certain language features don't work with properties. One example is taking the address of a data member and assigning it to a pointer-to-member.

Let me say up front that I dislike this style of programming with properties. The subtle advantage of "hiding" a private data member by treating it as a public data member escapes me. And from the object-oriented-design point of view, I think they are highly suspect. One of the fundamental principles of OO design is that behaviour should be visible; state should not. To conceal behaviour by masquerading it as a data member is just encouraging bad habits.

Properties are syntactic saccharine for getter and setter member functions for individual fields in a class. Of course, in the quest for good design, replacing a public data member with simple get/set functions for that data member does not achieve a large increase in abstraction. A function-call syntax which does not overtly refer to manipulating data members may lead to a cleaner interface. It allows the possibility that some "data members" may be computed at runtime rather than stored. An overwhelming majority of books on OO design recommend thinking in terms of objects' behaviour, while hiding their state. Let us encourage good habits, not bad ones.

UK C++ panel member Alisdair Meredith, with almost a decade's daily experience using the Borland VCL properties, had these comments:

Properties work fantastically well in RAD development where you want interactive tools beyond a simple source code editor. For all they appear glorified get/set syntax, they make the life of the component writer much simpler. There are no strange coding conventions to follow so that things magically work, and a lot of boilerplate code vanishes. From this perspective they are most definitely A Good Thing™.
Of course the property concept goes way beyond simply supporting GUI tools, and that is where the slippery slope begins...
If functional programming is 'programming without side effects', property oriented programming is the other extreme. Everything relies on side effects that occur as you update state. For example: you have a Leftproperty in your GUI editor to determine position of a control. How would you move this control at runtime? Traditionally we might write some kind of Move() function, but now we can set the Left property instead and that will somehow magically move the control on the form as a side-effect, and maybe trigger other events with callbacks into user code as a consequence.
Experience shows that people prefer to update the Left property rather than look for some other function. After all, that is how you would perform the same task at design/compile time. Generally, code becomes manipulating properties (and expecting the side effects) rather than making explicit function calls. Again, this is the 'minimum interface' principle so that there is one and only one simple way of achieving a given result. Typically, the Move() function is never written, and this reinforces the programming style.
As we move beyond the GUI-tools arena, I find the property syntax becomes more and more confusing. I can no longer know by simple inspection of a function implementation if I can take the address of everything used as a variable, or even pass them by reference. This only gets worse when templates enter the mix.

And the problem becomes worse yet when developers become fond of using write-only properties - values which can be set to achieve the side effect, but can never be queried.

The one-sentence summary of his experience is that using properties in non-GUI classes did not help productivity: "Properties made our code easier to write, but immensely more difficult to maintain." My own experience in trying to debug code with properties bears this out. While stepping through some code to look for leaks of memory and COM interfaces, I was examining all variables by hovering the mouse over them. Multiple hovers over the same variable (or what appeared to be a simple variable) showed up a data structure with different values each time, because the underlying property function was creating an additional COM interface with each access.

My impression is that the main benefit envisioned for properties is not their syntactic sleight-of-hand but (1) their ability to be discovered through introspection/reflection and manipulated non-programmatically by some sort of Rapid Application Development tool, and (2) their ability to be stored (I think "pickled" is the term used with Java Beans) in this configured state, so that they can be loaded at runtime, already configured. [1] appears to take for granted that these features should come as a package, if standard C++ were to be extended to embrace properties.

However I might feel about using properties, I have to recognise that many people do find them useful, at least for certain types of applications. For people who do like this style of programming, at least some of the benefits can be achieved through library classes without changing the C++ language and compilers. The accompanying sample code defines some utility class templates which may be used as members of domain classes:

Property

a read-write property with data store and automatically generated get/set functions. This is what C++/CLI calls a trivial scalar property.

ROProperty

a read-only property calling a user-defined getter.

WOProperty

a write-only property calling a user-defined setter.

RWProperty

a read-write property which invokes user-defined functions.

IndexedProperty

a read-write named property with indexed access to own data store.

For programmer convenience these classes offer three redundant syntaxes for accessing their data members (or apparent data members - properties need not have real storage behind them):

  • function call syntax

  • get/set functions

  • operator = (T) and operator T() for assignment to and from properties.

The read-only and write-only property classes implement only the accessors appropriate to their semantics, but for compatibility with C++/CLI they do reserve (unimplemented) the unnecessary get or set identifier.

Instances of the utility templates as outlined in this paper do have an address, they have a type for template deduction, and they can be used in a chain of assignments. They can also be used with today's compilers and debuggers. If someone brings forward a RAD tool to read and manipulate properties through metadata, then a "property" modifier or some other marker for the tool could be added to their declaration to help in generating the metadata.

This technique, described in its basic form in C++ Report back in 1995[1], seems at least as convenient, and produces as economical source code, as most uses of properties. The basic assignment and return functions can be inlined, and therefore should be efficient. In order to support chaining, the setter functions return their new value, but for complete compatibility with C++/CLI they should have a void return type.

One objection to these that has been raised is that CLI properties allegedly take up no space inside a class, whereas a C++ subobject cannot have a size of 0 (ignoring certain clever optimizations). On the other hand, I expect that most useful properties will need some way to preserve state between set and get, and that has to take up space somewhere. The size of Property takes up as much space as a single object of its template parameter, while the other three hold only a single pointer to an implementation object. Note that the Object and member function template parameters do not have to refer to the containing object, though that is likely to be the most common usage. They could delegate the processing to an external or nested helper class. The choice of implementation object (though not its type or member functions) can even be changed dynamically, and several objects could share a single implementation object.

The biggest inconvenience to these classes that I perceive is that all but the simplest Property instances need to be initialized at runtime with the address of their implementation object (usually the containing class). A smaller inconvenience is that using them on the right hand side of an assignment invokes a user-defined conversion, which could affect overload resolution and a sequence of conversions.

One of the features of C++/CLI properties is that they can be declared as virtual or pure virtual, for polymorphic overriding by derived classes. This obviously is not possible with data member declarations. But if the implementation object (which usually means the containing object) is itself polymorphic and the designated member functions are virtual, then the property will behave in a polymorphic fashion.

The C++/CLI feature of default (nameless) indexed properties can be simulated with a member operator [] (except that in C++/CLI indexes can take multiple arguments, but there is a separate proposal for C++ to allow comma-separated arguments inside square brackets).

Aside from matters of stylistic preference, I can see two possible scenarios in which these classes might be useful. One scenario would be in source code which needs to be portable between C++/CLI and standard C++ environments. Using the Property templates on the standard C++ side could compensate for a lack of compiler support for properties.

The second scenario would be for migrating code which uses public data members to a more encapsulated style. The class definitions could be rewritten to use the Property templates, while client code need not be rewritten, only recompiled.

A brief sample program (at the end of this article) shows the utility classes in use. As it illustrates three different styles of setting and getting a property value, the expected output is this:

Name = Pinkie Platypus
Name = Kuddly Koala
Name = Willie Wombat

ID = 12345678
ID = 9999
ID = 42

Children = 42
Children = 42
Children = 42

WO function myClass::addWeight called with
                                value 2.71828
WO function myClass::addWeight called with
                                value 3.14159
WO function myClass::addWeight called with
                                value 1.61803
Secretkey = *****
Secretkey = !!!!!
Secretkey = ?????

Convenor = Herb
Minutes = Daveed
Coffee = Francis

Initialisation Methods for Properties

Overload Technical Reviewer Phil Bass raised this question while reviewing the article for publication:

The ROProperty, WOProperty and RWProperty classes provide a function call operator for initialisation. Why don't they have constructors instead?

After looking over the code now (it was originally written last April) I have added a default constructor which makes sure the pointers are set to 0 for safety. But they still need to have an initialisation function. Initialising them, either with a named function or an overloaded function call operator, gives runtime flexibility. These are the benefits that occur to me:

  • If the implementation object were initialised in a constructor, it would create a dependency on the prior existence of the implementation object. In a complex application where such dependencies might be chained or even circular, this would be a nuisance.

  • The implementation object can be rebound. I don't know of any specific use cases that would drive this, but the capability falls out of doing it this way and might prove useful.

  • One implementation object could serve several different instances of objects with a property. Again, it just falls out. If pressed to come up with a use case, I would look for a situation where the 'property' has to be extracted from some heavyweight situation - a database, a large table in memory, a remote procedure call, or some kind of bottleneck where localised optimisations like caching could make a difference. This encapsulates the resource issues and shares the benefits around.

  • Since the function call operator (or some initialisation function) has to exist anyway, adding a constructor call which takes a value might confuse some programmers as to which they were calling. It wouldn't confuse the compiler - NumberOfChildren(p) in an initialiser list for myClass would invoke the constructor, whereas NumberOfChildren(this) inside a myClass constructor is calling the operator()() - but they look alike in code and many people are kind of vague about such details (which is a reason why many people avoid function objects).

  • The combination of the first and last points above brings us to the REAL reason why we don't initialise it in a constructor: the most common use case is likely to be that the enclosing object exposing the property provides its own implementation for it. If we need to initialise a data member with a constructor, we would naturally expect to do so in the initialiser list of the enclosing object's constructor, like this:

myClass() : NumberOfChildren(this),
            WeightedValue(this),
            Secretkey(this) {}

But as the myClass instance doesn't really exist before its constructor is entered, using its this pointer as an argument to the member constructors in the initialiser list seems, well, dubious. Of my three test compilers only MSVC++ gave a warning that "the this pointer is only valid within nonstatic member functions." All three compiled and ran this program, but I wouldn't want to rely on that as proof of correctness. I'm afraid I did not devise a stress test involving a polymorphic hierarchy to see how far I could push it before something broke.

I did think hard about what syntax in a property class constructor would be able to deduce the address of the property's enclosing object as a default - that would be really useful! - but concluded there is no way to say that in C++. So, since the user of the class will have to initialise it at some point, not having a constructor means that initialisation has to go into the body of the enclosing object's constructor rather than its initialiser list. I opted for the function call syntax as more concise than a named member function.

I admit that all this was written as a proof-of-concept rather than well-tested production code. For industrial-strength use, one needs to envision pathological scenarios that might arise. If the overloaded setter using function-call syntax ever takes a parameter of the same type as the implementation object (value has type Object *), there is going to be an ambiguity with the initialiser; for this situation the solution is a named initialiser member function and/or a named set() function. Another low-probability scenario (which means one that is guaranteed to bite you sooner or later) is using an external implementation object which is itself a const instance. The workaround for this would be another, slightly different, utility class template wrapping a const Object * my_object;

One of the advantages of implementing properties as library classes instead of a built-in language feature is that the programmer can vary or extend them as needed!

// Some utility templates for emulating
// properties - preferring a library solution
// to a new language feature
// Each property has three sets of redundant
// acccessors:
// 1. function call syntax
// 2. get() and set() functions
// 3. overloaded operator =

// a read-write property with data store and
// automatically generated get/set functions.
// this is what C++/CLI calls a trivial scalar
// property
template <class T>
class Property {
  T data;
public:

  // access with function call syntax
  Property() : data() { }
  T operator()() const {
    return data;
  }
  T operator()(T const & value) {
    data = value;
    return data;
  }

  // access with get()/set() syntax
  T get() const {
    return data;
  }
  T set(T const & value) {
    data = value;
    return data;
  }

  // access with '=' sign
  // in an industrial-strength library,
  // specializations for appropriate types
  // might choose to add combined operators
  // like +=, etc.
  operator T() const {
    return data;
  }
  T operator=(T const & value) {
    data = value;
    return data;
  }
  typedef T value_type;
            // might be useful for template
            // deductions
};

// a read-only property calling a
// user-defined getter
template <typename T, typename Object,
          T (Object::*real_getter)()>
class ROProperty {
  Object * my_object;
public:
  ROProperty() : my_object(0) {}
  ROProperty(Object * me = 0)
               : my_object(me) {}

  // this function must be called by the
  // containing class, normally in a
  // constructor, to initialize the
  // ROProperty so it knows where its
  // real implementation code can be
  // found.
  // obj is usually the containing
  // class, but need not be; it could be a
  // special implementation object.
  void operator()(Object * obj) {
    my_object = obj;
  }

  // function call syntax
  T operator()() const {
    return (my_object->*real_getter)();
  }

  // get/set syntax
  T get() const {
    return (my_object->*real_getter)();
  }
  void set(T const & value);
            // reserved but not implemented,
            // per C++/CLI

  // use on rhs of '='
  operator T() const {
    return (my_object->*real_getter)();
  }

  typedef T value_type;
            // might be useful for template
            // deductions
};

// a write-only property calling a
// user-defined setter
template <class T, class Object,
          T (Object::*real_setter)(T const &)>
class WOProperty {
  Object * my_object;
public:
  WOProperty() : my_object(0) {}
  WOProperty(Object * me = 0)
               : my_object(me) {}

  // this function must be called by the
  // containing class, normally in a
  // constructor, to initialize the
  // WOProperty so it knows where its real
  // implementation code can be found
  void operator()(Object * obj) {
    my_object = obj;
  }
  // function call syntax
  T operator()(T const & value) {
    return (my_object->*real_setter)(value);
  }
  // get/set syntax
  T get() const;
            // reserved but not implemented,
            // per C++/CLI
  T set(T const & value) {
    return (my_object->*real_setter)(value);
  }

  // access with '=' sign
  T operator=(T const & value) {
    return (my_object->*real_setter)(value);
  }

  typedef T value_type;
            // might be useful for template
            // deductions
};

// a read-write property which invokes
// user-defined functions
template <class T,
          class Object,
          T (Object::*real_getter)(),
          T (Object::*real_setter)(T const &)>
class RWProperty {
  Object * my_object;
public:
  RWProperty() : my_object(0) {}
  RWProperty(Object * me = 0)
               : my_object(me) {}

  // this function must be called by the
  // containing class, normally in a
  // constructor, to initialize the
  // ROProperty so it knows where its
  // real implementation code can be
  // found
  void operator()(Object * obj) {
    my_object = obj;
  }

  // function call syntax
  T operator()() const {
    return (my_object->*real_getter)();
  }
  T operator()(T const & value) {
    return (my_object->*real_setter)(value);
  }

  // get/set syntax
  T get() const {
    return (my_object->*real_getter)();
  }
  T set(T const & value) {
    return (my_object->*real_setter)(value);
  }
  // access with '=' sign
  operator T() const {
    return (my_object->*real_getter)();
  }
  T operator=(T const & value) {
    return (my_object->*real_setter)(value);
  }

  typedef T value_type;
            // might be useful for template
            // deductions
};

// a read/write property providing indexed
// access.
// this class simply encapsulates a std::map
// and changes its interface to functions
// consistent with the other property<>
// classes.
// note that the interface combines certain
// limitations of std::map with
// some others from indexed properties as
// I understand them.
// an example of the first is that
// operator[] on a map will insert a
// key/value pair if it isn't already there.
// A consequence of this is that it can't
// be a const member function (and therefore
// you cannot access a const map using
// operator [].)
// an example of the second is that indexed
// properties do not appear to have any
// facility for erasing key/value pairs
// from the container.
// C++/CLI properties can have
// multi-dimensional indexes: prop[2,3].
// This is not allowed by the current rules
// of standard C++
#include <map>
template <class Key,
          class T,
          class Compare = std::less<Key>,
          class Allocator
               = std::allocator<std::pair<
                           const Key, T> > >
class IndexedProperty {
  std::map<Key, T, Compare,
           Allocator> data;
  typedef typename std::map<Key, T, Compare,
                        Allocator>::iterator
          map_iterator;
public:

  // function call syntax
  T operator()(Key const & key) {
    std::pair<map_iterator, bool> result;
    result
      = data.insert(std::make_pair(key, T()));
    return (*result.first).second;
  }
  T operator()(Key const & key,
               T const & t) {
    std::pair<map_iterator, bool> result;
    result
      = data.insert(std::make_pair(key, t));
    return (*result.first).second;
  }

  // get/set syntax
  T get_Item(Key const & key) {
    std::pair<map_iterator, bool> result;
    result
      = data.insert(std::make_pair(key, T()));
    return (*result.first).second;
  }
  T set_Item(Key const & key,
             T const & t) {
    std::pair<map_iterator, bool> result;
    result
      = data.insert(std::make_pair(key, t));
    return (*result.first).second;
  }

  // operator [] syntax
  T& operator[](Key const & key) {
    return (*((data.insert(make_pair(
                   key, T()))).first)).second;
  }
};


// =================================
// and this shows how Properties are
// accessed:
// =================================

#include <string>
#include <iostream>

class myClass {
private:
  Property<std::string> secretkey_;

  // --user-defined implementation functions--
  // in order to use these as parameters,
  // the compiler needs to see them
  // before they are used as template
  // arguments. It is possible to get rid
  // of this order dependency by writing
  // the templates with slight
  // differences, but then the program
  // must initialize them with the
  // function addresses at run time.

  // myKids is the real get function
  // supporting NumberOfChildren
  // property
  int myKids() {
    return 42;
  }
  // addWeight is the real set function
  // supporting WeightedValue property
  float addWeight(float const & value) {
    std::cout << "WO function "
              << "myClass::addWeight "
              << "called with value "
              << value
              << std::endl;
    return value;
  }
  // setSecretkey and getSecretkey support
  // the Secretkey property
  std::string setSecretkey(
                     const std::string& key) {

    // extra processing steps here

    return secretkey_(key);
  }
  std::string getSecretkey() {

    // extra processing steps here

    return secretkey_();
  }

public:
  // Name and ID are read-write properties
  // with automatic data store
  Property<std::string> Name;
  Property<long> ID;

  // Number_of_children is a read-only
  // property
  ROProperty<int, myClass,
           &myClass::myKids> NumberOfChildren;

  // WeightedValue is a write-only
  // property
  WOProperty<float, myClass,
           &myClass::addWeight> WeightedValue;

  // Secretkey is a read-write property
  // calling user-defined functions
  RWProperty<std::string, myClass,
           &myClass::getSecretkey,
           &myClass::setSecretkey> Secretkey;

  IndexedProperty<std::string,
                  std::string> Assignments;

  // constructor for this myClass object
  // must notify member properties
  // what object they belong to
  myClass() {
    NumberOfChildren(this);
    WeightedValue(this);
    Secretkey(this);
  }
};
int main() {
  myClass thing;

  // Property<> members can be accessed
  // with function syntax ...
  thing.Name("Pinkie Platypus");
  std::string s1 = thing.Name();
  std::cout << "Name = "
            << s1
            << std::endl;

  // ... or with set/get syntax ...
  thing.Name.set("Kuddly Koala");
  s1 = thing.Name.get();
  std::cout << "Name = "
            << s1
            << std::endl;

  // ... or with the assignment operator
  thing.Name = "Willie Wombat";
  s1 = thing.Name;
  std::cout << "Name = "
            << s1
            << std::endl;
  std::cout << std::endl;

  // The same applies to Property<> members
  // wrapping different data types
  thing.ID(12345678);
  long id = thing.ID();
  std::cout << "ID = "
            << id
            << std::endl;

  thing.ID.set(9999);
  id = thing.ID.get();
  std::cout << "ID = "
            << id
            << std::endl;

  thing.ID = 42;
  id = thing.ID;
  std::cout << "ID = "
            << id
            << std::endl;
  std::cout << std::endl;

  // And to ROProperty<> members
  int brats = thing.NumberOfChildren();
  std::cout << "Children = "
            << brats
            << std::endl;

  brats = thing.NumberOfChildren.get();
  std::cout << "Children = "
            << brats
            << std::endl;

  brats = thing.NumberOfChildren;
  std::cout << "Children = "
            << brats
            << std::endl;
  std::cout << std::endl;

  // And WOProperty<> members
  thing.WeightedValue(2.71828);

  thing.WeightedValue.set(3.14159);

  thing.WeightedValue = 1.618034;
  std::cout << std::endl;

  // and RWProperty<> members
  thing.Secretkey("*****");
  std::string key = thing.Secretkey();
  std::cout << "Secretkey = "
            << key
            << std::endl;

  thing.Secretkey.set("!!!!!");
  key = thing.Secretkey.get();
  std::cout << "Secretkey = "
            << key
            << std::endl;

  thing.Secretkey = "?????";
  key = thing.Secretkey;
  std::cout << "Secretkey = "
            << key
            << std::endl;
  std::cout << std::endl;

  // and IndexedProperty<> members.
  // Multiple indices in square brackets
  // not supported yet
  thing.Assignments("Convenor",
                    "Herb");
  std::string job = thing.Assignments(
                                 "Convenor");
  std::cout << "Convenor = "
            << job
            << std::endl;

  thing.Assignments.set_Item("Minutes",
                             "Daveed");
  job = thing.Assignments.get_Item(
                                 "Minutes");
  std::cout << "Minutes = "
            << job
            << std::endl;

  thing.Assignments["Coffee"] = "Francis";
  job = thing.Assignments["Coffee"];
  std::cout << "Coffee = "
            << job
            << std::endl;
  std::cout << std::endl;

  return 0;
}

References#

[Wiegley] John Wiegley, PME: Properties, Methods, and Events. http://www.open-std.org/jtc1/sc22/wg21/docs/ papers/2002/n1384.pdf

[Vandevoorde] David Vandevoorde, C++/CLI Properties. http://www.open-std.org/jtc1/sc22/wg21/docs/ papers/2004/n1600.html

posted @ 2019-08-08 23:34  日月王  阅读(186)  评论(0编辑  收藏  举报