NOTE: Every post ends with "END OF POST". If you don't see it then open the full post in a separate page!

Emulating in, out and inout function parameters – Part 1

Emulating in, out and inout function parameters in C++.

In C++, passing arguments to functions can be done in a variety of ways. If you are not careful, even thou your function works as intended, the way its parameters are declared can easily mislead the caller.

Consider the following simple examples:

void f(string *s);
void g(string &s);

void caller() {
  auto a1 = string{"hello"};

Is function f going to change s? Is it so that the writer meant “const char *” but the const qualifier is missing by mistake?
Same applies to function g. Also, when calling g, it is not apparent that it may change a1. It looks like it takes a1 by value.

These and similar issues can be avoided by laying down coding conventions about “how to name functions” and “how to declare function parameters” for your team members in your project. The problem with such guidelines is that it may be hard to follow. Even in simple cases they may not be obvious. For example, we may know that our compiler can copy std::vector by merely copying a pointer. So you automatically declare the input parameter like this:

void f(vector<string> v);

On the other hand, other people may not know this. In this case, they tend to declare the same kind of input parameter like this:

void f(const vector<string>& v);

Both of these are correct but it leads to inconsistency and confusion. Also, guidelines about “how to declare function parameters” can become quite complex considering when and how to use pointers, references, const qualifiers, pass-by-value, etc in function declarations.

99% of the time, we can categorize function parameters as

  • Input: Parameters that are only read by the function. They are not changed.
  • Output: Parameters that are only written by the function. The value of the corresponding argument that the caller passes in is not relevant to the function. These parameters are products of the function and the caller sees their new values when the function returns.
  • Input and output: Parameters that are both read and written by the function.

Some languages, like C#, provide standard tools for specifying these categories for function parameters. C++ does not provide standard tools for this. Fortunately, C++ is a very flexible language and we can roll our own tools to achieve this.

Here is one way to implement such a feature:

#pragma once
namespace param_inout {
    // Input
    template <typename T> class inp;
    template <typename T> inp<T> ina(const T&);
    template <typename T> inp<T> ina(const inp<T>&);
    template <typename T>
    class inp {
        inp(inp&& other) : m_arg{other.m_arg} { /* empty */ }
        operator const T&() const { return m_arg; }
        const T& arg() const { return m_arg; }
        inp(const inp&) = delete;
        inp(const T& arg) : m_arg{arg} { /* empty */ }
        friend inp<T> ina<T>(const T&);
        friend inp<T> ina<T>(const inp<T>& arg);
        const T& m_arg;
    template <typename T>
    inp<T> ina(const T& arg) { return inp<T>{arg}; }
    template <typename T>
    inp<T> ina(const inp<T>& param) { return inp<T>{param.m_arg}; }

    // Output
    template <typename T> class outp;
    template <typename T> outp<T> outa(T&);
    template <typename T> outp<T> outa(outp<T>&);
    template <typename T>
    class outp {
        outp(outp&& other) : m_arg{other.m_arg} { /* empty */ }
        outp& operator=(const T& otherArg) { m_arg = otherArg; return *this; }
        outp(const outp&) = delete;
        outp(T& arg) : m_arg{arg} { /* empty */ }
        friend outp<T> outa<T>(T&);
        friend outp<T> outa<T>(outp<T>&);
        T& m_arg;
    template <typename T>
    outp<T> outa(T& arg) { return outp<T>{arg}; }
    template <typename T>
    outp<T> outa(outp<T>& param) { return outp<T>{param.m_arg}; }

    // Input and output
    template <typename T> class inoutp;
    template <typename T> inoutp<T> inouta(T&);
    template <typename T> inoutp<T> inouta(inoutp<T>&);
    template <typename T>
    class inoutp {
        inoutp(inoutp&& other) : m_arg{other.m_arg} { /* empty */ }
        operator T&() { return m_arg; }
        T& arg() const { return m_arg; }
        inoutp& operator=(const T& otherArg) { m_arg = otherArg; return *this; }
        inoutp(const inoutp&) = delete;
        inoutp(T& arg) : m_arg{arg} { /* empty */ }
        friend inoutp<T> inouta<T>(T&);
        friend inoutp<T> inouta<T>(inoutp<T>&);
        T& m_arg;
    template <typename T>
    inoutp<T> inouta(T& arg) { return inoutp<T>{arg}; }
    template <typename T>
    inoutp<T> inouta(inoutp<T>& param) { return inoutp<T>{param.m_arg}; }

These simple classes wrap around references to the actual function arguments. Why?

  • For Output and Input-Output parameters, taking a reference is necessary since we want to write into the arguments and make those writes visible to the caller.
  • For Input parameters, we take a const reference. It is a reference because most of the time it is “efficient enough”. It is const because it is a reference and we want it to be read-only. If we want, we can specialize it for simple types, like char or int, to store a copy and not a reference. For the sake of consistency and simplicity, even if you choose to specialize it, it’s probably better to always treat inp<T> as a reference. That is, make a copy of its contents (inp<T>.arg()) if you want to save it somewhere after the function returns.

In Part 2 of this post, we look at how these classes can be used.
Continue to Part 2.



Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s