• Keine Ergebnisse gefunden

2.4 Advanced C++ Programming Techniques

2.4.3 Improved Language Support in C++11

Template meta programming in C++ is not a feature that was designed to be part of the language; as already established above, the technique was discovered by chance in 1995 by Unruh [118]. Nevertheless, its power was quickly recognized by the community and it became the subject of intensive research [120, 119, 3] that went so far as to prove that the template mechanism in C++ forms a Turing-complete language – executed within the compiler.

Despite the success of template meta programming, the technique clearly suffers from the fact that it “abuses” C++ to do things the language was not designed for. This lack of language-level support has created idiosyncrasies like the fact that template meta functions are actually structs that “return” their value in a nested type or having to resort to obscure language properties like Substitution Failure Is Not An Error (SFINAE), which is a more powerful (but also harder to understand) alternative to template specialization. Taken together, these idiosyncrasies make

TMPcode very difficult to understand for programmers without experience in the field. Moreover, there are important gaps in functionality. The most obvious of those hard restrictions are probably the limitation of arithmetic to integer types and the requirement that a template argument list must be of fixed length.

As template meta programming was slowly starting to get recognized as one of the strengths of C++, the International Standards Organization (ISO) standardization committee, during its work on the C++11 revision of the language, added a number of new language features that simplify the writing of template meta programs. In the following, we highlight some of those features that are of particular importance in the scope of this work.

Variadic Templates

One of the major challenges when implementing containers for a variable number of elements of different type (e.g. a tuple) in C++03 is the lack of templates with a variable number of arguments. To work around this limitation, implementations have to choose a fixed upper bound to the number of arguments the data structure can accept and design the interface with this maximum number of arguments;

shorter variants can then be realized by defaulting the template arguments to a special marker type signaling an empty argument and specializing the data structure accordingly:

1 // marker for empty argument slots 2 struct empty {};

3

4 // standard tuple implementation for full number of arguments 5 template<typename T1 = empty, typename T2 = empty>

6 struct tuple { /* implementation */ };

7

8 // one-argument tuple 9 template<typename T1>

10 struct tuple<T1,empty> { /* implementation */ };

11

12 // zero-argument tuple 13 template<>

14 struct tuple<empty,empty> { /* implementation */ };

While this pattern does work, it has major drawbacks in that it leads to massive code repetition (remember that the actual bodies of the individual variants all need to be fully spelled out), and care has to be taken to apply changes and bug fixes to all specializations. As pointed out by Gregor and Järvi [59], the resulting code is hard to maintain, causing developers to resort to preprocessor meta programming [77].

By contrast, variadic templates allow an arbitrary number of template arguments by adding template argument packs to the language. With these packs, our tuple type becomes

1 template<typename... T>

2 struct tuple { /* implementation */ };

and we do not require any specializations anymore. Moreover, all the unused template parameters in the original version create very long type signatures which in turn cause severe performance issues for compilers. In the example presented in [77] (the tuple library from TR1), compile times increase exponentially with the maximum number of arguments permitted, which the authors contrast with a reimplementation based on variadic templates that avoids those limits completely while outperforming the original version even if the latter was limited to a maximum of three arguments.

Variadic template are still a rather advanced tool in the C++ toolbox; the variadic template argument packs cannot be accessed directly, but have to be unpacked in algorithms that recursively peel off individual elements of the pack by means of partial specialization or function overloading. For example, a function that takes an arbitrary number of arguments and prints each argument to the console could be implemented like this:

1 // recursion terminator, matches an empty argument list 2 template<typename... T>

3 void print(const T&... t)

4 {}

5

6 // extract the first element from the pack 7 template<typename T1, typename... T>

8 void print(const T1& t1, const T&... t)

9 {

10 std::cout << t1 << std::endl;

11 // recurse into tail of the pack 12 print(t...);

13 }

2.4 • Advanced C++ Programming Techniques 33 While this programming style takes some getting used to, variadic templates have been an invaluable tool for the implementation of the more advanced features of the TypeTree library.

Type Deduction With auto and decltype

C++03 lacks a mechanism to capture the return type of a function or functor, which complicates the creation of type-agnostic, stackable functors, which are of major importance in the design of expression templates and similar constructs.

To work around this problem, a library-level protocol was devised to obtain this information:

1 template<typename Expr>

2 struct negate {

3 template<typename T>

4 struct result_of {

5 typedef typename Expr::template result_of<T>::type type;

6 }

7

8 template<typename T>

9 typename result_of<T>::type operator()(const T& t) const { 10 return - expr(t);

11 }

12

13 Expr epr;

14 };

A user of thenegatefunctor will then have to invoke theresult_ofmeta function to determine its return type, which it needs to know in order to store the return value. While this protocol works and has been used in major frameworks over the years (e.g. Boost MPL[61]), it is exceedingly fragile, as programmers have to take care not to forget the result_of meta function, which can be problematic when integrating 3rd-party code. C++11 greatly simplifies the creation of this type of generic functor with the help of the new decltype keyword and a new syntax for function declarations. Using these features (and rvalue references to ensure perfect forwarding, another deficiency of the original implementation), the above example becomes

1 template<typename Expr>

2 struct negate {

3 template<typename T>

4 auto operator()(T&& t)

5 -> decltype(expr(std::forward<T>(t))) const {

6 return - expr(std::forward<T>(t));

7 }

8

9 Expr epr;

10 };

At a more mundane level, the newauto keyword allows programmers to omit the type of a newly declared variable if its type can be deduced from the initializer expressions. Consider the following example:

1 auto i = vec,size(); // i becomes a std::size_t 2 auto f = std::exp(2.2); // f becomes a double

In general, automakes the vast majority of explicit type declarations in user code redundant, greatly improving code readability, especially if those types have to be extracted from typedefs nested inside other variables.

While auto can be used to automatically deduce the type of a newly declared variable, its counterpartdecltype makes it possible to store a deduced type in a typedef or use it as a template parameter, a feature that was used in the example above and that forms the basis of a new type of static polymorphism for template meta functions introduced in Chapter 5.

C h a p t e r

3

Conforming Subdomains for DUNE Grids

In order to simulate multi domain problems like those mentioned in Chapter 1 (FSI, Stokes–Darcy coupling etc.), we have to manage separate meshes for those spatial subdomains within our application. We also need an efficient method for calculating the overlaps and intersections between those subdomains (as we will later see, the individual subproblems within our overall multi physics model couple via integrals over those intersections). There are two fundamental approaches to this problem:

• We can discretize each subdomain individually. This way, we are able to tailor each mesh optimally to the underlying problem. Moreover, it becomes possible to reuse existing software packages for the subproblems, even if those packages use incompatible data formats. On the other hand, this approach introduces a lot of additional complexity in order to calculate the intersections between those unrelated meshes and to transfer spatial data (e.g. solutions) between the subdomains. Despite those challenges, this architecture is used by all established software frameworks in this area, e.g. [20,45,47,89], mostly due to the offered flexibility and being able to integrate existing software with a mostly black-box approach.

• Alternatively, we can start with a single, global mesh for the entire (multi domain) simulation and then designate subdomains by marking the applicable cells in that overall mesh. This mostly inverts the advantages and disadvan-tages of the other strategy: All parts of the simulation have to be based on a common mesh data structure, and in areas where subproblems overlap, we cannot pick individual tessellations that are optimal for each of those

subproblems. The major upsides of this approach are vastly reduced software complexity (in terms of the coupling of subproblems) and the possibility to investigate both loosely coupled and monolithic solvers at the algebra level.

With this thesis, we aim at simplifying the development and investigation of new numerical solution schemes for a wide variety of problem classes. For that reason, we have chosen the second strategy and have created the subdomain-aware grid manager MultiDomainGridthat forms the basis of our multi domain framework.

In this chapter, we describe the functionality of this grid manager (realized as a DUNE module) and outline its implementation. In particular, we highlight its performance characteristics and present a way of tailoring the grid to specific problems by means of a modular backend engine. The information in this chapter is based on and expands our previous work in [96].

Remark 3.1. The C++ classes that implement the grid manager introduced in this chapter are located in the namespace Dune::mdgrid. In order to improve the readability of the code snippets in this chapter, we will omit this namespace and assume that the two namespace Dune andDune::mdgrid have been imported with appropriate using declarations.

3.1 Introduction

MultiDomainGrid has been developed as an add-on module forDune-Grid and can be found in the DUNE module Dune-Multidomaingrid. It is free software and available under the same licence as the DUNE core modules (the GNU General Public Licence with a special runtime exception, for further details see [40]). In addition to Dune-Grid and its dependencies, an installation of the Boost C++

libraries [114] is also required. In particular, the code uses the Boost packages MTL and Fusion.

The following description is based on version 2.3.1 of the library, which can be downloaded from [93] or directly from the source code repository at [94]. It requires the corresponding 2.3.1 release of the DUNE core modules.