Refactoring: Sort Members by Visibility

You have a C++ class that shows readers its implementation details before it shows readers its public interface.

Reorder the class members in order of decreasing visibility, preserving the relative order of the declarations within each visibility category.

Motivation

C++ does not provide for a builtin way of separating interface from implementation. A class declaration declares all of its members in one place, regardless of their visibility. Private members only intended for use by the class itself and protected members intended to support inheritance are both listed in the class header file, unless a mechanism like pimpl is used. Because protected and private members are not of interest to public clients of a class, it is desirable to declare these implementation details after the public interface for a class. This makes it easier to understand the class by reading its declaration. Similarly, protected members should be declared before private ones. The same reasoning applies to structs and unions, with the main difference being that the default visibility for a class is private and the default visibility for a struct or union is public.

Mechanics

  1. Create three comment lines at the top of the class declaration, one each for public, protected and private declarations.
  2. For each access qualifier in order of public, protected and private:
    1. Identify the first group of declarations at the current access level.
    2. Cut the entire block of lines containing the group of declarations onto the clipboard.
    3. Paste the declarations before the associated access qualifier comment line.
    4. If there are more declarations at this visibility level, go to step 2.1.
  3. Delete the comment lines created in step 1.
  4. Adjust the order of initializer lists in constructors so that initializer lists represent the new order of declarations of data members.
  5. If initialization order dependencies between data members prevent them from being properly initialized in their new declaration order, revert the entire change and abandon the refactoring. Address the order dependency separately first and then retry this refactoring.

Example

Here is a class taken from clang.

  /// DIScope - A base class for various scopes.
  class DIScope : public DIDescriptor {
  protected:
    friend class DIDescriptor;
    void printInternal(raw_ostream &OS) const;
  public:
    explicit DIScope(const MDNode *N = 0) : DIDescriptor (N) {}

    /// Gets the parent scope for this scope node or returns a
    /// default constructed scope.
    DIScope getContext() const;
    StringRef getFilename() const;
    StringRef getDirectory() const;
  };

1. Create comment markers.

  /// DIScope - A base class for various scopes.
  class DIScope : public DIDescriptor {
  // public
  // protected
  // private
  protected:
    friend class DIDescriptor;
    void printInternal(raw_ostream &OS) const;
  public:
    explicit DIScope(const MDNode *N = 0) : DIDescriptor (N) {}

    /// Gets the parent scope for this scope node or returns a
    /// default constructed scope.
    DIScope getContext() const;
    StringRef getFilename() const;
    StringRef getDirectory() const;
  };

2.1. Find the first block of public declarations.

  public:
    explicit DIScope(const MDNode *N = 0) : DIDescriptor (N) {}

    /// Gets the parent scope for this scope node or returns a
    /// default constructed scope.
    DIScope getContext() const;
    StringRef getFilename() const;
    StringRef getDirectory() const;

2.2. Cut the declarations.

  class DIScope : public DIDescriptor {
  // public
  // protected
  // private
  protected:
    friend class DIDescriptor;
    void printInternal(raw_ostream &OS) const;
  };

2.3. Paste the declarations just before the comment line for its visibility level.

  class DIScope : public DIDescriptor {
  public:
    explicit DIScope(const MDNode *N = 0) : DIDescriptor (N) {}

    /// Gets the parent scope for this scope node or returns a
    /// default constructed scope.
    DIScope getContext() const;
    StringRef getFilename() const;
    StringRef getDirectory() const;
  // public
  // protected
  // private
  protected:
    friend class DIDescriptor;
    void printInternal(raw_ostream &OS) const;
  };

2.4. Repeat for remaining public declarations.

There are no remaining declarations at public visibility, so we are done.

2.1. Find the first block of protected declarations.

  protected:
    friend class DIDescriptor;
    void printInternal(raw_ostream &OS) const;

2.2. Cut the declarations.

  class DIScope : public DIDescriptor {
  public:
    explicit DIScope(const MDNode *N = 0) : DIDescriptor (N) {}

    /// Gets the parent scope for this scope node or returns a
    /// default constructed scope.
    DIScope getContext() const;
    StringRef getFilename() const;
    StringRef getDirectory() const;
  // public
  // protected
  // private
  };

2.3. Paste the declarations just before the comment line for its visibility level.

  class DIScope : public DIDescriptor {
  public:
    explicit DIScope(const MDNode *N = 0) : DIDescriptor (N) {}

    /// Gets the parent scope for this scope node or returns a
    /// default constructed scope.
    DIScope getContext() const;
    StringRef getFilename() const;
    StringRef getDirectory() const;
  // public
  protected:
    friend class DIDescriptor;
    void printInternal(raw_ostream &OS) const;
  // protected
  // private
  };

2.4. Repeat for remaining protected declarations.

There are no more protected declarations and no private declarations, so we are done moving groups of declarations.

3. Delete the comment lines.

  class DIScope : public DIDescriptor {
  public:
    explicit DIScope(const MDNode *N = 0) : DIDescriptor (N) {}

    /// Gets the parent scope for this scope node or returns a
    /// default constructed scope.
    DIScope getContext() const;
    StringRef getFilename() const;
    StringRef getDirectory() const;
  protected:
    friend class DIDescriptor;
    void printInternal(raw_ostream &OS) const;
  };

4. Adjust initializer lists.

In this example, there is only a single initializer from a parameter, so there is no need to reorder the initialization expressions.

This completes the refactoring.

2 Responses to “Refactoring: Sort Members by Visibility”

  1. Paul Koning Says:

    I’m not sure why you would want to do this. To make the source code prettier? That works. To allow you to write abbreviated headers that do not mention the private members at all? That doesn’t work. For one thing, the compiler is not obliged to put the members in the same order across visibilities, only within a visibility.

    Like

    • legalize Says:

      When I am reading the declaration for a class it’s because I want to understand the class. As a consumer of the class, implementation details are a distraction and get in the way of understanding the public interface. I have added some additional text to the motivation that I hope makes this clear. It’s about reading the class declaration in order to comprehend the public interface.

      Like


Leave a comment