Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

C++ & operator []=


April, 2005: C++ & operator []=

Suggested changes to std::map


In this article, I suggest that the current language definition of the C++ subscript operator operator [] is too coarse grained. To illustrate this point, I examine std::map and describe several problems with its design. While these problems could have been avoided without changes to the language, they nonetheless provide stimulus for a discussion of the utility of a new operator—the assigning subscript operator operator []=. Like a lot of programmers, I don't like language-imposed constraints or compiler-specific solutions, so I also examine several measures that may be used to implement custom associative containers and/or address the shortcomings in std::map within the bounds of the current definition of the C++ language and the constraints of current compiler technology.

State-Dependent Semantics

The C++ Standard Library associative container std::map supports mapping between unique keys and their corresponding values. An element in the map is a key-value pair. (The type of the container's key is key_type, and the type of the values is mapped_type. Slightly confusingly, the container's value_type is the key-value pair—pair<const key_type, value_type>—in keeping with the STL convention that a container's value_type is the type of a dereferenced iterator.) std::map provides a subscript operator for accessing elements via an array-like syntax. Alas, the semantics provided are something of a double-edged sword. When used for the insertion/update of a new/existing element, the syntax is both convenient and elegant, as it is meaningful and safe:

std::map<std::string, short> iqs;
// A { name => IQ } mapping
iqs["Albert"] = 732;

This statement does one of two things, both with the same result (if successful). If no element exists with the key "Albert", then a new element is inserted. If one does exist with that key, then its value is changed to 732. In this context, the overloading of the subscript operator tallies with the semantics you observe with built-in arrays:

char letters[26];
letters[0] = "a";
...
letters[25] = "z";

However, the other, equally common, use for the subscript operator is to access the element already existing at the given index:

std::cout << letters[8] << letters[16];
// prints "iq"

Unfortunately, this is where the std::map falls down. Its subscript operator takes a key (of its key_type) and returns a mutable reference to its corresponding value (of its mapped_type):

template <...>
class map
{
...
mapped_type &operator
[](key_type const &key);

If no slot exists, one is created. The problem occurs when there is no existing entry for the key. Thus:

cout << "Matthew's IQ: " << iqs["Matty"] << endl;
// prints "0"

Because I have not yet inserted a value, the apparent access of my IQ in the earlier statement actually causes a new entry to be made, whose value is set to the default initialized value for a short (0). This is admittedly a little parsimonious, but arguably appropriate commentary for my having made such a misuse of the std::map.

It appears that we have a serious problem. The subscript operator serves element insertion/update perfectly, but it only serves element retrieval if insertion has already occurred. If not, then it serves as a kind of default insertion, very easily missed. This is bad because it violates the Rule of Transparency (see The Art Of UNIX Programming, by Eric Raymond, Addison-Wesley, 2003) of code: You cannot tell by examining a particular fragment of code whether it's using an std::map correctly. std::map appears to "do as the ints do" (see More Effective C++, Scott Meyers, Addison-Wesley, 1996) some of the time, but does not do so at other times—and the compiler cannot help you. Indeed, the compiler is positively prevented from being of assistance.

const-truculence

There's a related problem. Because the subscript operator has to be a mutating (nonconst) method to support its update and insertion semantics, it cannot be called on const instances:

inline void dump_my_IQ
(std::map<std::string, short> const &m)
{
std::cout << m["Matty"]; // compile error!
}

Hence, performing lookup on standard associative containers is a complex operation:

inline void dump_my_IQ
(std::map<std::string, short> const &m)
{
std::map<std::string, short>::const_iterator it =
m.find("Matty");
if(m.end() != it)
{
throw std::out_of_range("invalid key");
}
std::cout << (*it).second;
}

Yuck! Sadly, there's not even a nonoperator method that performs the lookup and returns a reference to the value, throwing an exception if it's not found. There's the added disadvantage of the potential lack of consistency between uses of this technique, as programmers may potentially elect to use different exception types. Of course, you could generalize in the form of an Attribute Shim ("Generalized String Manipulation: Access Shims and Type Tunneling," C/C++ Users Journal, August 2003; and Imperfect C++, Addison-Wesley 2004, both by Matthew Wilson) to handle this but that has its own set of problems.

Why does std::map not provide both mutating (nonconst) and nonmutating (const) forms of the subscript operator? The reason is that const/nonconst overload method resolution is done on the basis of the argument types, and the hidden pointer is treated in the same way as other arguments. Consequently, all other arguments being equivalent, the const overload is only selected when this is const; that is, when the instance is const. Hence:

std::map<std::string, short> m1 = ...;
const std::map<std::string, short> m2 = ...;
m1["Bill"] = 16;
// Calls nonconst version
cout << m2["Ben"] << endl;
// Calls const version
cout << m1["Bill"] << endl;
// Calls nonconst version!

Ironically, there is a method that can be used instead of the subscript operator when used for insertion/update: insert(). And use of it rather than the subscript operator would result in code that is almost equally clear:

iqs.insert(std::make_pair("Albert", 732));

In hindsight, the designers of the associative containers could have opted for insert() + operator []() const, and curtailed all possibilities for hidden semantic gotchas and verbose lookup code.

Separation Anxiety

The third problem with the use of the subscript operator is that of the separation of the creation of the element from the setting of the element's value. Looking back to the insertion of "Albert" into the original iqs map, what actually happens is:

  1. You construct an std::string instance from the literal string "Albert".
  2. You call std::map::operator [](), passing the string by reference. Assuming "Albert" is not already in the map, a new element is created and inserted whose value is defaulted (for example, =mapped_type()). A reference to that value is then returned.
  3. The calling code invokes the mapped_type's assignment operator, passing the right side of the statement.

This two-phase operation to the insertion of a new element has three drawbacks:

  • No validation. The element's value is assigned after the element is created, and outside the map. This is not an issue for std::map because it does not do any kind of value validation. But any custom container that would seek to do validation cannot do so if it follows std::map's semantics. The same obstacle would be present to a custom container that seeks to avoid redundancy by referring all identical values to the same instance.
  • Unsafe exceptions. If the mapped_type's assignment operator throws in step 3, then you've got a map with meaningless contents. That's not of much help in writing try/catch-free exception-safe code! Using the std::pair<key_type, mapped_type> form of insert() with make_pair() does not have this issue.
  • Inefficient. Because the element's value is first constructed and then assigned, there is a potential inefficiency, especially for mapped_types of class type.

Other Languages

Before exploring alternatives, it's worth noting how other languages address this issue.

Java doesn't have operator overloading, so the issue is moot. Although it's disconcertingly ugly and verbose, Java succeeds here in not suffering as C++ does. Its Map/Map<K, V> interface has put() and get() methods to insert/update and lookup, respectively.

C# has indexers, which let a class instance be indexed in a manner syntactically similar to arrays. Essentially, an indexer block encapsulates either or both of get and set subblocks. They are semantically equivalent to two separate functions for insert/update and lookup.

D, Heron, Python, and Ruby all handle the situation with far greater aplomb than C++, making a distinction between insertion/update and retrieval by separating the two into distinct methods. D has opIndexAssign() and opIndex(). Heron has _op_subscript_read and _op_subscript_write. Python has __set_item__ and __get_item__. Ruby has "[]=" and "[]". All of these are eminently useful and usable, and I've never had any ambiguous experiences with any of them.

operator []= for C++

If I had free reign to extend the language, I might suggest the addition of an assigning subscript operator, operator []=. With this, the compiler could be directed to make a simple and unambiguous choice between insertion/update via the assigning form (operator []=), and lookup via the standard form (operator []). When the subscript appears to the left of an assignment, the assigning form would be called; otherwise, the standard form would be used. The standard form might still come in mutating (nonconst) and/or nonmutating (const) form, but classes that implemented those methods would not elect to perform insertion when they're invoked: Either the element exists, in which case the operator returns a (const) reference to the value; otherwise, an exception is thrown.

iqs["Matty"] = 16; // Calls operator []=()
short iq = iqs["Matty"];
// Calls operator []() const

For classes that provide both insertion/update and retrieval, you'd see these two operators defined together as follows:

class IQMap
{
...
/// Subscripting operators
public:
// Assigning subscript operator
<whatever-you-want> operator []=
(key_type const &key,
mapped_type const &val);
// Subscript operator
mapped_type
&operator [](key_type const &key);
mapped_type const
&operator [](key_type const &key) const;
...
};

It's as simple as that! The further boon is that you can now overload operator [] on const/nonconst because the two overloads have identical behavior and are only distinguished by the constness of their return types.

This provides for identical semantics in the subscript operator between standard containers and those that provide collection-like interfaces to third-party libraries (e.g., http://stlsoft.org/), or nonmodifiable views over other collections. The one drawback to current practice is that expressions such as iqs["Frazier"]++ would no longer work in the case where the elements do not currently exist. However, that's a minor loss compared to the big gains to be had.

Pragmatic Measures in the "Now"

Okay, operator []= is a (possibly) nice idea, you say, but we don't have it. Moreover, even if it was added to C++ in C++-0x, we're still years away from seeing it in compilers.

Being a pragmatist, I have several ideas for how we might get a safer, more sensible semantic for associative containers now.

Adopt the operator [] () const approach. This is a great idea. Change operator [] to a (throwing) const form, and ambiguity and retrieval inconveniences disappear forever. Unfortunately, there's a rub—the millions of lines of existing C++ code. There'd be no place to hide for the standards committee, and all my friends would stop talking to me.

Unambiguous operator [] overloads? Remember, I said that we can't overload std::maps operator[] for const/nonconst because they factually do different things, and it's difficult to ensure that the non-mutating (const) form is called at all times when it's intended. Well, there is a way—using argument decoration (see Listing One). Simply put, the two normal operator [] overloads perform the throwing lookup you're after, while the one overloaded to take an inserter<> performs the insertion. You would use it as follows:

umap<std::string, short> iqs;
iqs["Matty"] = 63;
// A compiler error at last. Hurrah!
iqs[insert("Matty")] = 63;
// Works fine, and not the least bit
// ambiguous

std::cout << iqs["Matty"] << endl;
// "63"
std::cout << iqs["Boris"] << endl;
// throws std::out_of_range

There's no ambiguity. It's clear what's going on, and there's no loss of efficiency in the argument because all compilers can easily optimize-out such reference copying. Furthermore, for the argument-forwarding aficionados, there's no ambiguous copying or const subversion (as described in Chapter 21 of Imperfect C++, since the arguments are always key_type const& for both the container's argument and the argument proxy type. This can be used for new containers, but it still can not be retrofitted to std::map without breaking code. Note that this solves the state-dependency and const-lookup issues, but the separation issues remain.

Add an at() lookup method. Just because you can't get a better operator [] without getting the C++ world offside, you don't need to give up. You can still have a simple throwing lookup. The sequence (STL Tutorial and Reference Guide, by David Musser et al., Addison-Wesley, 1996) containers—for example, std::basic_string, std::vector—have an at() method that validates the requested index and throws std::out_of_range if it's invalid. Custom containers and std::map could do the same:

namespace std
{
template <...>
class map
{
...
mapped_type const
&at(key_type const &key) const;
mapped_type
&at(key_type const &key);
...

This is a solution that could work without breaking any existing code, and would make life simpler for users of associative containers. (Incidentally, the at() member function has already been suggested for inclusion in the Standard Library.)

Now you can rewrite the lookup in an equally readable form, which will either correctly look up the element or will kindly throw an exception:

cout << "Matthew's IQ:"
<< iqs.at("Matty") << endl;

Use veneers on std::map. This approach arguably has the most practical use because it allows you to modify existing std::map classes now. Basically, you provide alternatives to the standard associative containers that are syntactically and semantically identical, save for the modified operator [] and at() methods. One way to do this is by implementing the map in terms of an std::map member, and forwarding all identical methods directly to the contained member (see Listing Two).

An alternative is to define a veneer for associative containers that provides the required semantic adjustments/enhancements. Veneers (which are a specialized form of the Adapter pattern; see Design Patterns, Erich Gamma et al., Addison-Wesley, 1995) are templates that derive from their primary parameterizing types, add no nonstatic member variables, and respect the polymorphic nature of their primary parameterizing type. Listing Three implements a veneer.

Both of these approaches are useful now, and will reveal any issues at compile time. The state dependency and const-lookup issues are handled as shown in Listings Two and Three, and you can easily overload (or hide) the insert() methods such that you have a two-parameter form taking key and value, should you so choose.

Return proxies from operator []. One of my reviewers suggested that operator [] can be written to return a proxy object, which will then only cause an insertion if subject to an assignment. (Scott Meyers describes a similar technique for deferring changes to reference-counted strings in Item #29 of More Effective C++.) That's a nice idea, with some unfortunate flaws.

First, a conforming container must return by reference to support the taking of the address of the returned value, so the proxy object would have to overload the address of operator operator &. I have such deep antipathy to overloading the operator &, that I simply couldn't live with that option. (See Chapter 26 of Imperfect C++ for the complete rant and rave.)

The other objection is that to support the two semantics—insertion/update and lookup—the proxy would have to have nontrivial logic. As a library writer, I know that complex implementations are not appreciated, neither by myself who has to write and maintain them, nor by users who have to understand them.

Furthermore, such things are potentially inefficient. Although it's unlikely to be the case with most modern compilers, there is still the possibility that the implementations may not be fully inlined and elided during the optimization phase of code generation. What is likely to be much less avoidable is the cost of separate initialization and assignment forced by the assignment to the reference

The obvious theoretical objection is that proxies can no more help with validation of the value than does the current nonconst subscript operator provided by std::map. Of course, you could write a specific proxy class, or the validation policy could be specified as an additional template parameter to your container template, but that's just adding even more to the complexity of the implementation.

There's not a single killer objection to the use of the proxy, but I'd suggest that the combination of these points represents a pretty unattractive picture.

Use std::map as is, remembering your lookup shims. The path of least resistance is, of course, to go with current semantics, which may be the only option for people who are not in a position to adopt new tools. In this case, I'd suggest using insert() for insertion and update, and either the m.find() == m.end() ? "no-op":"dereference" idiom or the std::out_of_range-throwing attribute shims for retrieval. In this way, you can avoid std::map's operator []() entirely, and dispense with the heartache.

Conclusion

I've illustrated the problems inherent in the design of the Standard Library std::map containers, and deficiencies in the language itself, in so far as the subscript operator does not adequately reflect the semantics of built-in types. There are hidden semantic pitfalls and tedious lookup logic. I've suggested that a simple enhancement to the language—the addition of operator []=—would resolve all these issues. Given the likely long lead times for any such language changes, even if adopted, to filter down to real programmers, I've suggested other, more practical and immediately realizable measures that can help clear the semantic fog of operator [] and std::map right now.

Acknowledgments

Thanks to Bjorn Karlsson, Christopher Diggins, Chuck Allison, Garth Lancaster, Greg Peet, John Torjo, Nevin Liber, Thorsten Ottosen, and Walter Bright for criticism and encouragement.

DDJ



Listing One

// Alternative op [] semantics, via argument decoration
template<class T>
struct inserter
{
  inserter(T const &arg)
    : argument(arg)
  {}
  T const &argument;
};

// "maker" function
template<class T>
inline inserter<T> insert(T const &arg)
{
  return inserter<T>(arg);
}
template< typename K // Key type
        , typename R // Reference type
        , . . .
        >
class umap
{
  . . .
  // Some "standard" map methods, used in the new ones below
  std::pair<iterator, bool> insert(value_type const &value);
  iterator                  find(key_type const &key);
  const_iterator            find(key_type const &key) const;

  /// This method looks up the value for the given key
  /// \note This does <b>not</b> perform insertions
  referent_type const &operator [](key_type const &key) const
  {
    const_iterator  it  = this->find(key);
    if(this->end() == it)
    {
      throw new std::out_of_range("invalid key");
    }
    return (*it).second;
  }
  /// This method looks up the value for the given key
  /// \note This does <b>not</b> perform insertions
  referent_type   &operator [](key_type const &key)
  {
    iterator  it  = this->find(key);

    if(this->end() == it)
    {
      throw new std::out_of_range("invalid key");
    }
    return (*it).second;
  }
  /// Specialised override which performs insertions
  template<class T1>
  referent_type   &operator [](inserter<T1> const &key)
  {
    // How's this for a nice little statement ... ?? ;-)
    return (*this->insert(key.argument).first).second;
  }
  . . .
};
Back to article


Listing Two
// An unambiguous map: implemented in terms of std::map
  . . . // All the inserter / insert() stuff from Listing 1
template< typename K                     // Key type
        , typename R                     // Reference type
        , typename P = std::less<K>      // Comparison predicate
        , typename A = std::allocator<T> // Allocator
        >
class umap // unambiguous map. Not too cheeky, eh?
{
/// \name Types
/// @{
private:
  typedef std::map<K, T, P, A>                    map_impl_type;
public:
  typedef typename map_impl_type::key_type        key_type;
  typedef typename map_impl_type::referent_type   referent_type;
  typedef typename map_impl_type::key_compare     key_compare;
  typedef typename map_impl_type::allocator_type  allocator_type;
  typedef typename map_impl_type::value_type      value_type;
  typedef typename map_impl_type::value_compare   value_compare;
  typedef umap<K, T, P, A>                        class_type;
  typedef typename map_impl_type::size_type       size_type;
  typedef typename map_impl_type::difference_type difference_type;
  typedef typename map_impl_type::reference       reference;
  typedef typename map_impl_type::const_reference const_reference;
  typedef typename map_impl_type::iterator        iterator;
  typedef typename map_impl_type::const_iterator  const_iterator;
  typedef typename map_impl_type::reverse_iterator reverse_iterator;
  typedef typename map_impl_type::const_reverse_iterator  
                                             const_reverse_iterator;
/// @}
/// \name Construction
/// @{
public:
  explicit umap(key_compare const     &pred = key_compare()
              , allocator_type const  &ator = allocator_type())
    : m_map(pred, ator)
  {}
  umap(class_type const &rhs)
    : m_map(rhs.m_map)
  {}
#ifdef __STLSOFT_CF_MEMBER_TEMPLATE_CTOR_SUPPORT
  template <typename I>
  umap(I from, I to
#else /* ? __STLSOFT_CF_MEMBER_TEMPLATE_CTOR_SUPPORT */
  umap( value_type const *from, value_type const *to
#endif /* __STLSOFT_CF_MEMBER_TEMPLATE_CTOR_SUPPORT */
      , key_compare const &pred = key_compare()
      , allocator_type const &ator = allocator_type())
    : m_map(from, to, pred, ator)
  {}
/// @}
/// \name Member access and keyed-insertion operations
/// @{
  /// This method looks up the value for the given key
  /// \note This does <b>not</b> perform insertions
  referent_type const &operator [](key_type const &key) const
  {
    const_iterator  it  = m_map.find(key);
    if(m_map.end() == it)
    {
      throw new std::out_of_range("invalid key");
    }
    return (*it).second;
  }
  /// This method looks up the value for the given key
  /// \note This does <b>not</b> perform insertions
  referent_type   &operator [](key_type const &key)
  {
    iterator  it  = m_map.find(key);
    if(m_map.end() == it)
    {
      throw new std::out_of_range("invalid key");
    }
    return (*it).second;
  }
  /// Specialised override which performs insertions
  template<class T1>
  referent_type   &operator [](inserter<T1> const &key)
  {
    return m_map[key.argument];
  }
  referent_type       &at(key_type const &key)
  {
    return operator [](key);
  }
  referent_type const &at(key_type const &key) const
  {
    return operator [](key);
  }
  std::pair<iterator, ss_bool_t>  insert(value_type const &value)
  {
    return m_map.insert(value);
  }
  iterator                insert(iterator it, value_type const &value)
  {
    return m_map.insert(it, value);
  }
#ifdef __STLSOFT_CF_MEMBER_TEMPLATE_CTOR_SUPPORT
  template <typename I>
  void insert(I from, I to)
#else /* ? __STLSOFT_CF_MEMBER_TEMPLATE_CTOR_SUPPORT */
  void insert(value_type const *from, value_type const *to)
#endif /* __STLSOFT_CF_MEMBER_TEMPLATE_CTOR_SUPPORT */
  {
    return m_map.insert(from, to)
  }
/// @}
  // All of the rest of the methods forward directly to m_map
/// \name Operations
/// @{
public:
  iterator  erase(iterator it)
  {
    return m_map.erase(it);
  }
  iterator  erase(iterator first, iterator last);
  size_type erase(key_type const &key);
  void      clear();
  void      swap(class_type const &rhs);
/// @}
/// \name Attributes
/// @{
public:
  size_type       size() const
  {
    return m_map.size();
  }
  size_type       max_size() const;
  bool            empty() const;
  allocator_type  get_allocator() const;
  key_compare     key_comp() const
  value_compare   value_comp() const;
/// @}
/// \name Searching
/// @{
public:
  iterator                                  find(key_type const &key)
  {
    return m_map.find(key);
  }
  const_iterator                 find(key_type const &key) const;
  size_type                      count(key_type const &key) const;
  iterator                       lower_bound(key_type const &key);
  const_iterator                 lower_bound(key_type const &key) const;
  iterator                       upper_bound(key_type const &key);
  const_iterator                 upper_bound(key_type const &key) const;
  std::pair<iterator, iterator>  equal_range(key_type const &key);
  std::pair<const_iterator, const_iterator> 
                              equal_range(key_type const &key) const;
/// @}
/// \name Iteration
/// @{
public:
  iterator                begin();
  iterator                end();
  const_iterator          begin() const;
  const_iterator          end() const;
  reverse_iterator        rbegin();
  reverse_iterator        rend();
  const_reverse_iterator  rbegin() const;
  const_reverse_iterator  rend() const;
  {
    return m_map.rend();
  }
/// @}
private:
  map_impl_type m_map;
};
Back to article


Listing Three
// An unambiguous map: a veneer over std::map
  . . . // All the inserter / insert() stuff from Listing 1
template< typename K                     // Key type
        , typename R                     // Reference type
        , typename P = std::less<K>      // Comparison predicate
        , typename A = std::allocator<T> // Allocator
        >
class umap
  : public std::map<K, R, P, A> // Inherits publicly
{
/// \name Types
/// @{
private:
  typedef std::map<K, T, P, A>                       parent_class_type;
public:
  typedef typename parent_class_type::key_type       key_type;
  typedef typename parent_class_type::referent_type  referent_type;
  typedef typename parent_class_type::key_compare    key_compare;
  typedef typename parent_class_type::allocator_type allocator_type;
  typedef typename parent_class_type::value_type     value_type;
  typedef typename parent_class_type::value_compare  value_compare;
  typedef umap<K, T, P, A>                           class_type;
  typedef typename parent_class_type::size_type      size_type;
  typedef typename parent_class_type::difference_type difference_type;
  typedef typename parent_class_type::reference       reference;
  typedef typename parent_class_type::const_reference const_reference;
  typedef typename parent_class_type::iterator        iterator;
  typedef typename parent_class_type::const_iterator  const_iterator;
  typedef typename parent_class_type::reverse_iterator reverse_iterator;
  typedef typename parent_class_type:
             :const_reverse_iterator  const_reverse_iterator;
/// @}
/// \name Construction
/// @{
public:
  explicit umap(key_compare const     &pred = key_compare()
              , allocator_type const  &ator = allocator_type())
    : parent_class_type(pred, ator)
  {}
  umap(class_type const &rhs)
    : parent_class_type(rhs)
  {}
  template <typename I>
  umap( I from, I to
      , key_compare const &pred = key_compare()
      , allocator_type const &ator = allocator_type())
    : parent_class_type(from, to, pred, ator)
  {}
/// @}

/// \name Member access and keyed-insertion operations
/// @{
  /// This method looks up the value for the given key
  /// \note This does <b>not</b> perform insertions
  referent_type const &operator [](key_type const &key) const
  {
    const_iterator  it  = this->find(key);
    if(this->end() == it)
    {
      throw new std::out_of_range("invalid key");
    }
    return (*it).second;
  }
  /// This method looks up the value for the given key
  ///
  /// \note This does <b>not</b> perform insertions
  referent_type   &operator [](key_type const &key)
  {
    iterator  it  = this->find(key);

    if(this->end() == it)
    {
      throw new std::out_of_range("invalid key");
    }
    return (*it).second;
  }
  /// Specialised override which performs insertions
  template<class T1>
  referent_type   &operator [](inserter<T1> const &key)
  {
    // How's this for a nice little statement ... ?? ;-)
    return parent_class_type::operator [](key.argument);
  }
  referent_type       &at(key_type const &key)
  {
    return operator [](key);
  }
  referent_type const &at(key_type const &key) const
  {
    return operator [](key);
  }
  // All other methods are visible by inheritance from the veneer
  // base class.
};
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.