4.4. Concepts overview

One of the pitfalls when using templates, especially templates in unfamiliar libraries, is that C++ templates can be too generic. When you declare a template with either a class or typename, literally any class or typename could be passed in.

What if your template assumes one of the provided types has a push_back function? C++ has a mechanism to resolve this ambiguity and enforce additional requirements on template arguments used as parameters.

4.4.1. Concepts

Without resorting to the experimental technical specification you can still get some of the readability improvements from concepts, just by defining an alias for the type your template expects:

#include <iostream>
// A 'concept' for a numeric type
#define NumericType typename

// Same template as before
// 'typename' replaced with 'NumericType'
template <NumericType T, int N> T multiply (T val) {
  return val * N;
}

We haven’t actually made any functional change here. We have simply made a change that allows our source code to indicate our intent.

Until the Concepts/Constraints Technical Specification is implemented, expressing our intent is about all we can do.

#include <iostream>
#include <sstream>
#include <string>

#define InputIterator typename

namespace mesa {
  struct point {
    int x = 0;
    int y = 0;
  };

  // T must overload operator >>
  template <InputIterator T>
    T get(std::string prompt = "Enter a single value: ") {
      while(true) {
        std::cout << prompt;
        std::string line;
        std::getline(std::cin, line);

        // If we can't stream into our type T
        // then the input was not valid for that type.
        std::istringstream buf(line);
        T result;
        if(buf >> result) {
          // check for any extra input and reject input if found
          char junk;
          if(buf >> junk) {
            std::cerr << "Unexpected character.\n";
          } else {
            return result;
          }
        } else {
          std::cerr << "Not a valid input.\n";
        }
      }
    }
}

int main() {
  auto a = mesa::get<int>();
  auto b = mesa::get<int>("Enter an integer: ");
  auto c = mesa::get<float>("Enter a float: ");

  std::cout << "Values: " << a << ", "
                          << b << ", "
                          << c << '\n';

  // auto p = mesa::get<mesa::point>();  // compile error!
  return 0;
}

Attempting to ‘get’ a mesa::point is a compile error because our point object does not have a definition for the operator>> function overload. The compiler first displays the error, which may look something like this:

concept.cpp:25:16: error: invalid operands to binary expression ('std::istringstream' (aka 'basic_istringstream<char>') and 'mesa::point')
        if(buf >> result)
concept.cpp:41:18: note: in instantiation of function template specialization 'mesa::get<mesa::point>' requested here
  auto p = mesa::get<mesa::point>();

The compiler will then display an exhaustive list of every type it tried:

/usr/include/c++/v1/cstddef:135:3: note: candidate function template not viable: no known conversion from 'std::istringstream' (aka 'basic_istringstream<char>') to 'byte' for 1st argument
operator>> (byte  __lhs, _Integer __shift) noexcept
^
/usr/include/c++/v1/istream:625:1: note: candidate function template not viable: no known conversion from 'mesa::point' to 'unsigned char *' for 2nd argument
operator>>(basic_istream<char, _Traits>& __is, unsigned char* __s)
^
/usr/include/c++/v1/istream:633:1: note: candidate function template not viable: no known conversion from 'mesa::point' to 'signed char *' for 2nd argument
operator>>(basic_istream<char, _Traits>& __is, signed char* __s)
^

// many others omitted

The more code you have written and the more code pulled in from #include directives, the longer the list will be. It can run on for thousand of lines. Clearly we’d like something better, but the C++ standard doesn’t offer a solution until C++20.

4.4.2. Keyword: requires

A requires clause is an additional constraint on template arguments or a function. It is planned for release in C++20.

You will sometimes encounter named requirements in C++ code.

The named requirements listed are the named requirements used in the C++ standard to define the expectations of the standard library.

Some of these requirements are being formalized in C++20 using the concepts language feature. Until then, the burden is on the programmer to ensure that library templates are instantiated with template arguments that satisfy these requirements. Failure to do so may result in very complex compiler errors and warnings.

Even though they do not enforce any specific compiler rule or constraint (yet), they can improve the intent of expected template types.


More to Explore

You have attempted of activities on this page