iLab Neuromorphic Robotics Toolkit  0.1
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Programming guidelines

General rules and indentation

  • NRT uses doxygen for documentation, with the exclamation mark (!) comment style. You should learn about doxygen and look at how the rest of NRT is documented before you start writing code. Undocumented or improperly documented code is subject to deletion. Also see here for more explanations of tricky documentation cases (once you know the basics of doxygen).
  • NRT treats all warnings as errors. So your code will not compile if you have warnings. This is a feature: if your code has warnings, you are doing something wrong! If you are including files from a library not written by you and that has warnings, you can use the following:
NRT_BEGIN_UNCHECKED_INCLUDES
#include <trashystuff.h>
NRT_END_UNCHECKED_INCLUDES
  • Source code line length is 120. Add this to your ~/.emacs to set it:
  ;; set default line wrap len:
  (setq default-fill-column 120)
  • NRT uses astyle to ensure uniform indentation styles across our various text editors. A style file is included with NRT, and can be found in nrt/scripts/astylerc. To use this style file, invoke astyle using the –options flag, e.g.
astyle myfile --options=/path/to/nrt/scripts/astylerc
  • Indentation matching this style is as defined by the following emacs rules, on top of the default C mode of emacs indentation (ad this to your ~/.emacs to activate):
  ;; NRT indentation style for C++ and such
  (defun my-c-mode-common-hook ()
    (local-set-key "\C-h" 'backward-delete-char)
    ;; this will make sure spaces are used instead of tabs
    (setq tab-width 4 indent-tabs-mode nil)
    (setq indent-tabs-mode 'nil)
    (setq c-basic-offset 2)
    (c-set-offset 'substatement-open 0)
    (c-set-offset 'statement-case-open 0)
    (c-set-offset 'case-label 0)
    (c-set-offset 'brace-list-open 0)
    (c-set-offset 'access-label -2)
    (c-set-offset 'inclass 4)
    (c-set-offset 'member-init-intro 4)
    ;; include possible ! as comment start string so that indentation starts after it
    (setq comment-start-skip "/\\*+!* *\\|//+ *")
     
    ;; type C-c C-s or C-c C-o while editing to see what other rules to add here...
  )
  
  (add-hook 'c-mode-hook 'my-c-mode-common-hook)
  (add-hook 'c++-mode-hook 'my-c-mode-common-hook)
  (add-hook 'perl-mode-hook 'my-c-mode-common-hook)
  (add-hook 'cperl-mode-hook 'my-c-mode-common-hook)
  (add-hook 'emacs-lisp-mode-hook 'my-c-mode-common-hook)
  (add-hook 'nroff-mode-hook 'my-c-mode-common-hook)
  (add-hook 'tcl-mode-hook 'my-c-mode-common-hook)
  (add-hook 'makefile-mode-hook 'my-c-mode-common-hook)
  • If you use vi, use this configuration:
  "-------------Essential NRT Style Compliance Settings-------------
   
  " Disable old-school vi compatability
  set nocompatible
  
  " Allow plugins to control our indentation
  filetype plugin indent on
  
  " Set each auto-indent level to equal two spaces
  set shiftwidth=2
  
  " Let each tab equal two spaces
  set tabstop=2
  
  " Make sure vim turns all tabs into spaces
  set expandtab
  
  " Make vim indent our code properly
  set smartindent
  
  " Make the maximum line length equal 120
  set textwidth=120
  
  "-------------Other cool vim tricks-------------
  
  " Use a cool menu when autocompleting filenames, commands, etc...
  set wildmenu
  set wildmode=list:longest
  
  " Make vim automatically change directories to the directory of any file you open. 
  " This means that when you open a file, then want to open another using :tabe, :o, etc,
  " you can just type in the relative path from the file you're currently editing.
  set autochdir
  
  " When editing the NRT library, it is a total pain when you are editing a .H file in nrt/include/whatever/whatever, 
  " and then decide you need to edit the source .C file in the nrt/src/whatever/whatever. This little function will 
  " automatically back track in the directory tree for you, find the corresponding .C or .H file, and open it in a new
  " tab. 
  " To use it, just type ,o (that's a comma, and then a lower-case o). 
  function! OpenOther()
    if expand("%:e") == "C"
      exe "tabe" fnameescape(expand("%:p:r:s?src?include?").".H")
    elseif expand("%:e") == "H"
      exe "tabe" fnameescape(expand("%:p:r:s?include?src?").".C")
    endif
  endfunction
  nmap ,o :call OpenOther()<CR>

Files organization

  • NRT uses exclusively filename extensions .H and .C for C++ files.
  • NRT uses CamelCase convention for file names and for class names. An exception is helper classes used for template meta-programming, which usually use lowercase_with_underscores names, to resemble the STL (see more details below).
  • The NRT Library is mainly split into the nrt/src/ and nrt/include/ directories. These directories are then further split into Core, ImageProc, Robotics, etc. The build system will then create separate shared libraries for each of these directories, e.g. libnrtCore, libnrtImageProc, libnrtRobotics, etc. This segregation will allow users to pick and choose which functionality they care to compile down the line, and will help alleviate the 'dependency soup' that plagues big monolithic libraries. We have set up the following file organization scheme to help maintain this segregation as well as to maintain a high level of organization and documentability of the library:
    • Each directory under a shared library should contain files which can be logically grouped together - e.g. the Core subdirectory has subdirectories such as Memory, Geometry, Debugging, Blackboard, etc. Each of these subdirectories contains a group of classes which implement a specific feature.
    • All header files must be named with a .H extension, and should go into the include directory. Header files should be split such that all backend functionality which users don't need do know about should be hidden away in a details subdirectory. In general, such backend functionality can be split into a Helpers file which contains any helper classes and can be included before the body of a class definition, and a Impl implementation file which includes any inline code. For example, the Blackboard class defined in nrt/Core/Blackboard/Blackboard.H relies on some helper classes defined in nrt/Core/Blackboard/details/BlackboardHelpers.H, and has some inline implementation code defined in nrt/Core/Blackboard/details/BlackboardImpl.H. This organization makes it very easy for us to generate user documentation that omits all of the nasty implementation details.
    • All source files should be named with a .C extension, and should go into the src directory following the same relative path as it's .H file.
    • As much code as possible should be moved into .C files so that users don't have to recompile our .H files over and over again. The big exception to this is template code, which must go into .H files.
    • All header and source files should include the boilerplate preamble/license. Just copy it from another file.
  • Class member variables have names starting with "its" to indicate to humans reading the code that they are member variables.
  • NRT member functions use camelCase starting with a lowercase letter
  • NRT free functions usually use name_with_underscores
  • Generally speaking, one file per class. File name and class name must match exactly. If several classes conceptually belong together, then it is ok to put them in the same file.
  • All .H files use include guards. No .C file uses include guards. Include guard names are derived from both directory and file name, are all caps, and use underscores. The endif should tell us what it is ending in a comment. For example, in nrt/Core/Typing/Exception.H, we have:
#ifndef NRT_CORE_TYPING_EXCEPTION_H
#define NRT_CORE_TYPING_EXCEPTION_H
// ....
#endif // NRT_CORE_TYPING_EXCEPTION_H
  • A new class will hence typically involve the following set of files:
    • include/nrt/XXX/MyClass.H: only contains declarations and documentation. Absolutely no actual implementation code, except for one: the serialize or load/save functions. The rationale is that we want these functions to be as close as possible to the list of class data members, so we don't forget to update them if we add/remove data members. Also, only declare things of interest to the end user. Everything in this file should be documented using doxygen markup.
    • include/nrt/XXX/details/MyClassHelpers.H: contains supporting declarations that must be known before the main declarations in MyClass.H can take effect. For example, if the end user will only use a derived class and the base class contains no information that they should care about, declare the base class in details/MyClassHelpers.H and towards the top of MyClass.H include details/MyClassHelpers.H. There is no doxygen markup in this file, and documentation is optional, mainly geared towards advanced programmers.
    • include/nrt/XXX/details/MyClassImpl.H: contains inlined and template implementation ONLY. There is no doxygen markup in this file, and documentation is optional, mainly geared towards advanced programmers.
    • src/nrt/XXX/MyClass.C: contains all non-template, non-inline implementation. Documentation is optional.
    • include/nrt/XXX/details/MyClassInst.H: contains extern template statements. FIXME.
      Note
      The more you can put in your .C file and the less in your Impl.H file, the less you will slow down everybody's compile time.
      See for example the following files:
  • To find particular words in NRT source code, I recommend adding the following macro to your ~/.bash_aliases or ~/.bashrc:
    # do a grep on nrt sources:
    ng () {
      grep $* `${NRTHOME}/scripts/list-sources.sh`
    }
    

You can use it as follows, for example to find all files that refer to the Skeleton class:

itti@iLab1:~/nrt$ ng Skeleton
include/nrt/Graphics/ShapeRenderer.H:    namespace mocap { class Skeleton; }
include/nrt/Graphics/ShapeRenderer.H:        std::map<std::string, nrt::graphics::mocap::Skeleton> & skeletons();
include/nrt/Graphics/ShapeRenderer.H:        std::map<std::string, nrt::graphics::mocap::Skeleton> itsSkeletons;
...

Const Correctness

NRT uses exclusively the right-to-left convention for const qualifications. This is because it is the best way to unambiguously read statements that have const in them, just read them aloud from right to left. For example:

int const * prt1; // ptr1 is pointer to const int (read from right to left)
int * const ptr2; // ptr2 is a const pointer to int (cannot change the address, but can change the int value)
int const * const ptr3; // ptr3 is a const pointer to a const int (fixed address, fixed value)

See http://en.wikipedia.org/wiki/Const-correctness for more details and examples. Also see this one: http://www.dansaks.com/articles/1999-02%20const%20T%20vs%20T%20const.pdf and that one: http://www.parashift.com/c++-faq-lite/const-correctness.html

  • NRT is const-correct code (except for bugs and ommissions!). When writing nrt code, make sure it is const-correct. For example:
    • input arguments to a function are typically received by const reference. For example:
      void myfunc(std::string const & arg);
    • a member function that does not modify any member variables of an object should be declared const. For example:
      class MyClass
      {
      public:
      int getX() const
      { return x; } // since we do not modify x or anything else in MyClass, getX() is declared const
      private:
      int x;
      }
    • a temporary variable that will not be modified should be declared const. For example:
      double const perimeter = 2.0 * M_PI * radius; // assuming perimeter will not be modified later
    • a mutex in a class should typically be declared mutable, which will allow it to be locked/unlocked even in const member functions of the class.
    • For return values of functions: return a const ref if possible (i.e., you are returning a const ref to something which will not disappear soon, typically use this for accessor functions of your classes, when returning one of the data members of the class), otherwise return by value (non-const). While returning by non-const value may pose dangers, it also allows move semantics. This is particularly important if you return a vector, a string, etc as by returning it by non-const value it will actually be moved (in most cases) as opposed to copied.
    • on rare occasions, it is more desirable to keep a member function const even though it might modify some member variables of a class, if logically speaking this makes more sense. You have to use your judgments for these cases, and use a const_cast inside the function. For example, consider the BlackboardUID class:
      // ######################################################################
      //! Type for Blackboard unique ID data
      /*! This used internally by Blackboard ONLY to keep track of blackboards across distributed systems. Users should not
      be concerned with this. Because Blackboard is a Singleton, we will initialize our internals upon the first call to
      str(). */
      class BlackboardUID
      {
      public:
      //! As a string for printing debug messages and transmitting over networks
      std::string const & str() const;
      private:
      std::string strID; // the ID as a string
      };
      The str() function just returns the UID as a string, so it should be const. However, as noted in the comments, because Blackboard is a Singleton and it has a UID, there is a chicken-and-egg problem with initializing the UID, so we here decide to initialize it the first time it is used. Hence the implementation looks like this:
      std::string const & nrt::BlackboardUID::str() const
      {
      if (strID.empty())
      {
      char hn[256]; gethostname(hn, 256);
      std::ostringstream os;
      os << hn << '['
      << std::hex << std::noshowbase << (unsigned)gethostid() << "]:" << getpid()
      << ':' << (unsigned long)(&(nrt::Blackboard::instance()));
      const_cast<nrt::BlackboardUID *>(this)->strID = os.str();
      }
      return strID;
      }
      where we use const_cast to set member variable strID the first time it is requested.
    • see http://en.wikipedia.org/wiki/Const-correctness for more details and examples.

Local variables

  • Local variable names should favor longer more descriptive names over shorter ones. E.g.
// Bad...
for (int i = 0; i < c; ++i);
// Better...
for (int itemIdx = 0; itemIdx < itemCount; ++itemIdx);

Proper use of inline, virtual, etc

  • In declarations, do not write inline (e.g., in class declarations). Inline is an implementation detail and people just looking at your interface (declarations) should not be bothered with it.
  • In definitions (implementation of functions), that's the right place to add inline. For template functions, add inline to the same line as the template.

Example:

template <class T>
class Stuff
{
public:
void doit(); // NO INLINE HERE IN DECLARATION
};
...
// IN IMPLEMENTATION FILE:
template <class T> inline
Stuff<T>::doit()
{ ... }
  • The rule for virtual is the opposite as for inline: specify it in your declarations (users of your classes need to know what is virtual and can safely be reimplemented by derived classes), omit it from your definitions (once a function has been declared virtual, it will stay that way).

Capitalization rules

  • Class names should be written in CamelCase starting with an uppercase character, for example:
class MyCoolClass;
class ImageSegmenter;
\edncode
- Variables should be camelCase starting with a lowercase character, for example:
\code
bool isRunning;
size_t arraySize;
  • Member variables should be camelCase starting with the prefix “its”, for example:
class MyClass
{
size_t itsCounter;
std::vector<int> itsStorage;
}
  • Function names should be camelCase starting with a lowercase character, for example:
void doSomething(int paramOne);
class MyClass
{
void doSomethingElse(int paramTwo);
}
  • Typedef's which are just simple aliases for types should be CamelCase starting with an uppercase character, for example:
typedef nrt::Dims<float> FloatingDims;
  • Anything that is the result of any fancy metaprogramming (e.g. static const variables or typedefs set by compile-time checks) should be written in all lowercase with underscores separating the words, for example:
template<class T1, class T2>
struct simple_promotion
{
// Even though this is a typedef, we use lowercase and an underscore to emphasize
// that it is part of some fancy metaprogramming
typedef decltype(*(T1*)nullptr + *(T2*)nullptr)) promoted_type;
};
template<class PixType>
{
// Even though this is just a boolean, we use lowercase and an underscore to emphasize
// that it is part of some fancy metaprogramming
static bool const is_multi_channel = (PixType::numChannels > 1) ? true : false;
};

Differences compared to the iLab Neuromorphic Vision Toolkit (iNVT)

If you have used iNVT in the past, here are some key syntax differences:

In Image.H:

Operation INVT NRT
Get Image width img.getWidth() img.width()
Get Image height img.getHeight() img.height()
int/float img Image<float> Image<PixGray<float> >
get value (x,y) img.getVal(x,y) img.at(x,y)
set value (x,y) img.setVal(x,y,value) img(x,y) = value
get value from float img img.getVal(x,y) img.at(x,y).val()
Create Image Image<float> img(w,h,NO_INIT)Image<PixGray<float> >(w,h,ImageInitPolicy::None)
Check initialized img.initialized()img.size()>0
Get Writeable iterator img.beginw()img.begin()
Get Read-Only iterator img.begin()img.const_begin()

In Point.H:

Operation INVT NRT
Get x,y point.i, point.j point.x(), point.y()

There are also similar differences with Dims.H, Rectangle.H, etc

Writing image processing functions

  • Image processing function in NRT use the Image class template. The Image class has ref-counting and copy-on-write semantics, so you can treat it as you would a shared_ptr. In particular:
    • Pass Image arguments to functions by value if you will be internally modifying those images (so that the ref count is properly incremented)
    • It is ok to pass Image arguments that are only going to be read by const reference.
    • Do not hesitate to return images from your functions, the image will not be copied. This is preferred compared to passing a writable reference to an Image as argument. For example:
      template <typename Pix>
      void badFunction(nrt::Image<Pix> & input, nrt::Image<Pix> & output); // This is all bad...
      template <typename Pix>
      nrt::Image<Pix> goodFunction(nrt::Image<Pix> input); // This is all good...
      template <typename Pix>
      nrt::Image<Pix> goodFunction2(nrt::Image<Pix> const & input); // This is all good...
  • The Image class supports a variety of pixel types: different types of color channels (PixGray, PixRGB, PixHSV, etc) and different precisions (byte, uint16_t, int, long, float, double, etc). This has the following consequences:
    • In most cases, image processing functions should be generic function templates
    • For functions that can work on images with any Pixel type, use class PixType as the pixel type; for example:
      //! Apply a 3-point median filter in the x direction
      template <class PixType>
      Image<PixType> median3x(Image<PixType> const & in);
    • For functions that only work with some particular channel representation but different precisions, use class T as the underlying type for the specified pixels; for example, since the following function is to colorize a gray image, the input image should always have gray pixels, and the return image should always have color pixels:
      //! Colorize a grayscale image using a "jet" colormap
      template <class T>
      Image<PixRGB<byte> > jet(Image<PixGray<T> > const & input, T min, T max);
    • Try to avoid functions that only work on one specific pixel type and pixel precision, but if you ever need to write one, then it is not a template at all; for example:
      //! A function that only works on PixRGB<byte>
      void myFunc(Image<PixRGB<byte> > const & in);
  • Return types should be promoted according to the C/C++ integral type promotion rules. For example, a function that adds two images with byte pixels should have int pixels in the result. Several helper classes and macros are available in NRT to allow this; see Pixels.H, PixelTypes.H, and nrt/Core/Typing/Macros.H. The 3 macros below require that your input image pixels will be named PixType and your output image pixels will be named DestType:

    • NRT_PROMOTE_PIX(typ) if you want to promote to a type with at least the precision of typ as the default promotion; this basically defines DestType = typename nrt::promote<PixType, promo>::type as the default destination type and promo has a default value typ.
    • NRT_PROMOTE_PIX_NO_DEFAULT_PROMO if you want to use nrt::promote to compute the destination type but do not want to give a default; this basically defines DestType = typename nrt::promote<PixType, promo>::type but promo has no default value. Use this to remind users that some promotion will occur due to the operation, ane they need to think about what that should be and then call the proper version of your function (note that void is a valid promo value, see nrt::promote).
    • NRT_PROMOTE_PIX_NO_DEFAULT if you want no assumptions at all, users of your function will have to manually specify types for PixType and DestType

    For example:

    //! Low-pass filter, NxN applied in X and Y
    template <NRT_PROMOTE_PIX_NO_DEFAULT_PROMO>
    Image<DestType> lowPass(int const N, Image<PixType> const & src);

    The result image will have a promoted type, this is because this operation needs to compute weighted averages among pixels, so the weighted sum will have to be in a promoted type. By not providing a default promo, users will have to call the function as such:

    Image<PixRGB<byte> > source;
    Image<PixRGB<int> > result = lowPass<int>(3, source);
    Image<PixRGB<byte> > result2 = lowPass<void>(3, source); // internally promoted result converted back to PixRGB<byte>

    Note that with a void promotion the results are converted back to the same precision as the source, using nrt::clamped_convert (there is a CPU cost to this as it checks for overflows, e.g., an int value of 300 converted back to byte will be 255). See clamped_convert for details.

    Inside the implementation of your function, you should be able to only use PixType and DestType. For example:

    template <NRT_PROMOTE_PIX_NO_DEFAULT> inline
    {
    int const w = src.width(), h = src.height();
    // promote the source image to DestType if necessary, so that we do the promotion only once for all, rather than many
    // times as we access the pixels of the image; if no promotion is necessary, "source" will just point to the original
    // data of "src" through the copy-on-write/ref-counting behavior of Image:
    if (w < 2) return source; // nothing to smooth
    nrt::Image<DestType> result(source.dims());
    typename nrt::Image<DestType>::iterator dptr = result.begin();
    // Unfolded version (all particular cases treated) for max speed. do not use access overload since can access the C
    // array directly. the bet is: float computations are faster than int (true for most float copros), and int2float
    // converts fast->do not have to divide by the coeff sum since we directly use float coeffs. notations: in () is the
    // position of dest ptr, and ^ is src ptr
    // ########## horizontal pass
    for (int j = 0; j < h; ++ j)
    {
    // leftmost point [ (2^) 1 ] / 3
    dptr++ = sptr[0] * (2.0F / 3.0F) + sptr[1] * (1.0F / 3.0F);
    // rest of the line except last point [ 1^ (2) 1 ] / 4
    for (int i = 0; i < w - 2; ++ i)
    {
    dptr++ = (sptr[0] + sptr[2]) * 0.25F + sptr[1] * 0.5F;
    ++ sptr;
    }
    // last point [ 1^ (2) ] / 3
    dptr++ = sptr[0] * (1.0F / 3.0F) + sptr[1] * (2.0F / 3.0F);
    sptr += 2; // sptr back to same position as dptr
    }
    return result;
    }
  • You should always try to also write a GenericImage version of your image processing function. GenericImage represents an Image of any pixel type (or almost, there actually is a list in the declaration of GenericImage for all the supported pixels). GenericImage is what we pass between modules in messages, so that users do not have to insert annoying image conversion modules when they build a high-level system.

    To use GenericImage you need to:

    • 1) write your image processing function template using PixType and DestType as explained above;
    • 2) understand how boost::variant works;
    • 3) have a look at the GenericImage class declaration and understand how it works;
    • 4) understand the visitor design pattern;
    • 5) write a visitor that runs your image processing function template inside. Note that GenericImage supports two basic types of visitors: unary (one GenericImage input and possibly other arguments) and binary (two GenericImage inputs of possibly different types, plus possibly other arguments). Have a look in nrt/Core/ImageProc for examples of image processing functions that use GenericImage.