Cpp Notes

ch2.techniques

Ch 2. Techniques

The techniques have an important property: They are “nonterminal”; that is, you can combine them to generate higher-level idioms.

2.1 Compile-Time Assertions

#define STATIC_CHECK(expr, msg)                                             \
  {                                                                         \
    class ERROR_##msg {};                                                   \
    (void)sizeof(                                                           \
        (Chapter2::Ch2_1::CompileTimeChecker<(expr) != 0>(ERROR_##msg()))); \
  }
template <bool>
struct CompileTimeChecker {
  CompileTimeChecker(...);
};
template <>
struct CompileTimeChecker<false> {};
template <class To, class From>
To safe_reinterpret_cast(From from) {
  STATIC_CHECK(sizeof(From) <= sizeof(To), Destination_Type_Too_Narrow);
  return reinterpret_cast<To>(from);
}
... void* somePointer = ...;
char c = safe_reinterpret_cast<char>(somePointer);
  • The idea is to pass the compiler a language construct that is legal for a nonzero expression and illegal for an expression that evaluates to zero.
  • The code defines a local class called ERROR_Destination_Type_Too_Narrow that has an empty body.
  • Then, it creates a temporary value of type CompileTimeChecker<(sizeof(From) <= sizeof(To))>, initialized with a temporary value of type ERROR_ Destination_Type_Too_Narrow.
  • Finally, sizeof gauges the size of the resulting temporary value.
  • Now here’s the trick. The CompileTimeChecker<true> specialization has a constructor that accepts anything; it’s an ellipsis function.
    • This means that if the compile-time expression checked evaluates to true, the resulting program is valid.
    • If the comparison between sizes evaluates to false, a compile-time error occurs: The compiler cannot find a conversion from an ERROR_Destination_Type_Too_Narrow to a CompileTimeChecker<false>.
  • And the nicest thing of all is that a decent compiler outputs an error message such as Error: Cannot convert ERROR_Destination_Type_Too_Narrow to CompileTimeChecker <false>.

2.2 Partial Template Specialization

  • Typically, in a partial specialization of a class template, you specify only some of the template arguments and leave the other ones generic. When you instantiate the class template in your program, the compiler tries to find the best match. The matching algorithm is very intricate and accurate, allowing you to partially specialize in innovative ways.
  • When you instantiate a template, the compiler does a pattern matching of existing partial and total specializations to find the best candidate; this gives you enormous flexibility
  • Unfortunately, partial template specialization does not apply to functions—be they member or nonmember—which somewhat reduces the flexibility and the granularity of what you can do.
  • Although you can totally specialize member functions of a class template, you cannot partially specialize member functions.
  • You cannot partially specialize namespace-level (nonmember) template functions. The closest thing to partial specialization for namespace-level template functions is overloading. For practical purposes, this means that you have fine-grained specialization abilities only for the function parameters—not for the return value or for internally used types.
template <class T, class U>
T Fun(U obj);  // primary template

template <class U>
void Fun<void, U>(U obj);  // illegal partial
// This declaration attempts to provide a partial specialization for the Fun
// function template, where the return type T is explicitly specified as void.
// However, this is not allowed for function templates in C++. Partial
// specialization is only allowed for class templates, not function templates.

// specialization
template <class T>
T Fun(Window obj);  // legal (overloading)
// Instead of providing a partial specialization, you can achieve similar
// behavior by overloading the function template. This declaration introduces
// a new function template named Fun, specialized for the case when the
// function parameter is of type Window. This is legal and results in function
// overloading rather than specialization.

2.3 Local Classes

Local classes in C++ offer a specialized functionality with certain constraints and unique advantages.

  • They cannot have static member variables.
  • They are unable to access non-static local variables from their enclosing scope.

Their integration into template functions is notable, allowing them to utilize template parameters from those functions.

Despite the constraints, local classes contribute to code organization and symbol encapsulation.

They are inherently final, preventing external derivation and thus encapsulating functionality tightly within a function.

  • It's possible to replicate scenarios involving local classes with external template classes, suggesting local classes don't introduce new programming idioms.
  • However, they can streamline implementations and enhance the locality of symbol usage.
  • Local classes offer a method to ensure classes are final without needing unnamed namespaces or separate translation units.
class Interface {
 public:
  virtual void Fun() = 0;
  ...
};

template <class T, class P>
Interface* MakeAdapter(const T& obj, const P& arg) {
  class Local : public Interface {
   public:
    Local(const T& obj, const P& arg) : obj_(obj), arg_(arg) {}
    virtual void Fun() { obj_.Call(arg_); }

   private:
    T obj_;
    P arg_;
  };
  return new Local(obj, arg);
}

2.4 Mapping Integral Constants to Types

A simple template, can be very helpful to many generic programming idioms.

template <int v>
struct Int2Type {
  static const int value = v;
};
  • Int2Type generates a distinct type for each distinct constant integral value passed. This is because different template instantiations are distinct types; thus, Int2Type<0> is different from Int2Type<1>, and so on.
  • In addition, the value that generates the type is “saved” in the enum member value.
  • You can use Int2Type whenever you need to “typify” an integral constant quickly.
  • This way you can select different functions, depending on the result of a compile-time calculation. Effectively, you achieve static dispatching on a constant integral value. Typically, you use Int2Type when both of the following conditions are satisfied:
    • You need to call one of several different functions, depending on a compile-time constant.
    • You need to do this dispatch at compile time.
  • For dispatching at runtime, you can use simple if-else statements or the switch statement. The runtime cost is negligible in most cases.