Experiment wtih adding C++17 concepts/requirements to the dependency injection with templates example.

Introduction

Ever since I wrote the example of dependency injection using templates I thought I wanted to try using C++17 concepts/requirements.

To date, when using templates, compared with using vtable interfaces, one gives up ease of development/compiler enforcement to gain genericity and performance. I hoped that the concepts will allow the programmer to define the expectations on the injected classes. However this will add to the total number of lines of code, increasing them closer to the vtable example, and was wondering how the numbers will pan out.

What you need to try it now

I used GCC 6.0, the tip from the GCC github mirror, and build it using the hints from A. Sutton’s origin pages, on a Ubuntu 15.10 virtual machine. It needed some the trial and error. Other than the need to install additional packages (e.g. flex) I’ve learned that you get weird messages if the VM has only 1GB memory. 4GB is much better.

The rough steps I’ve followed are:

1
2
3
4
5
6
7
8
9
10
11
12
13
mkdir ~/src
cd ~/src
git clone https://github.com/gcc-mirror/gcc.git
cd gcc
./contrib/download_prerequisites
mkdir -p ~/build/gcc
cd ~/build/gcc
~/src/gcc/configure --prefix=~/opt/gcc6 --disable-bootstrap --disable-multilib --disable-nls --disable-werror --enable-languages=c,c++
make
make install
~/opt/gcc6/bin/gcc --version

export PATH="$HOME/opt/gcc6/bin:$PATH"

To compile a test program you need to pass the -fconcepts argument.

1
g++ -fconcepts main.cpp

Code

Here is one option to enforce the expectation of the house class on it’s template types. Lines 7 to 23 are the overhead of using concept/requirements.

house.h (injected types and references)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#pragma once

template<
  typename Cuppa,
  typename Door,
  typename Tv>
concept bool House()
{
  return requires(
    Cuppa c,
    Door d,
    Tv t)
  {
    {c.finish()} -> void;

    {d.open()} -> void;
    {d.close()} -> void;

    {t.switch_on()} -> void;
  };
}

House{Cuppa, Door, Tv}
class house
{
public:
  house(
    Cuppa & cuppa,
    Door & door,
    Tv & tv
    ) :
    cuppa_{ cuppa },
    door_{ door },
    tv_{ tv }
  {
  }

  void chillax()
  {
    cuppa_.finish();

    door_.open();
    door_.close();

    tv_.switch_on();
  }

private:
  Cuppa & cuppa_;
  Door & door_;
  Tv & tv_;
};

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <cuppa>
#include <door>
#include <tv>
#include <house>

int main()
{
  cuppa c;
  door d;
  tv t;
  house<cuppa, door, tv> h{ c, d, t };
  h.chillax();
}

Note that that there is more than one way of doing things. For example from the syntax point of view:

1
2
3
4
5
6
7
// House{Cuppa, Door, Tv} is shorthand for
template<
  typename Cuppa,
  typename Door,
  typename Tv
  >
  requires House<Cuppa, Door, Tv>()

Similarly one could have defined separate concepts for Cuppa, Door and Tv, instead of having them under House. In this case that would have been more verbose, with no gain.

Error

If the door class does not have a close method, one would now get a message like:

1
2
3
4
5
6
7
8
9
main.cpp: In function 'int main()':
main.cpp:11:24: error: template constraint failure
   house<cuppa, door, tv> h{ c, d, t };
                        ^
house.h:7:24: note:   constraints not satisfied
house.h:7:24: note:   concept 'House<cuppa, door, tv>()' was not satisfied
house.h:7:37: error: scalar object 'h' requires one element in initializer
   house<cuppa, door, tv> h{ c, d, t };
                                     ^

For this trivial example, the pre-concept error message would have been shorter.

1
2
3
4
5
main.cpp: In instantiation of 'void house<Cuppa, Door, Tv>::chillax() [with Cuppa = cuppa; Door = door; Tv = tv]':
main.cpp:11:13:   required from here
house.h:43:11: error: 'struct door' has no member named 'close'
     door_.close();
     ~~~~~~^~~~~

But the advantage of concepts is that one can easily require say the close method for door to be noexcept and return int: {d.close()} noexcept -> int;. Without concepts that would have not been possible to enforce at compile time.

Conclusion

The concepts syntax is reasonably compact and very powerful with regards to what it can enforce.

References

  • http://www.stroustrup.com/sle2011-concepts.pdf
  • http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3351.pdf
  • http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3701.pdf
  • http://asutton.github.io/origin/start.html