This page gathers the coding conventions used in the TFEL/MFront sources. It is written for new developers and for coding agents, which are expected to produce code indistinguishable from the existing sources.

The conventions described here apply to the C and C++ sources of all the components of the project (TFEL, MFront, MTest, TFELCheck, the various solver interfaces and the bindings). They complement the general contribution process described in the contributing guidelines.

Rules stated with shall are mandatory. When a rule is only partially satisfied by the historical sources, this is explicitly mentioned: a coding agent shall apply the rule to the code it writes or modifies, but shall not rename unrelated entities as a side effect.

Quick reference

Topic Rule
Formatting clang-format with the .clang-format file of the repository, ./format.sh before submitting
File name same name as the main class it declares, UpperCamelCase
File header doxygen comment describing the file followed by the copyright statement
Include order standard headers, then third-party headers, then TFEL headers, then headers of the current library, then own header
Types (classes, structures, aliases, enumerations) UpperCamelCase
Methods and free functions camelCase
Data members and variables snake_case
Template parameters UpperCamelCase (or a single letter for generic parameters)
Macros and enumerators ALL_UPPER_CASE, prefixed by the library name for macros
static constexpr constants UpperCamelCase, with a short lowercase alias for physical constants
Entity kinds struct (never class), public members first
Data members and methods always qualified by this->
Integer types no long nor long long, except for the API of a third-party library
Documentation every member, function and method documented with doxygen
Error handling tfel::raise, tfel::raise_if, exception classes, no macro
Closing comments } // end of <name> on namespaces, types, functions and enumerations
Backward compatibility a development which does not fix a bug but changes the results or the behaviour shall be handled by a compatibility mode

Tooling

Formatting

The sources are formatted with clang-format using the .clang-format file at the root of the project:

Language: Cpp
Standard: Cpp11
BasedOnStyle: Google
ColumnLimit: 80
Cpp11BracedListStyle: true
BinPackParameters: false
NamespaceIndentation: All
SortIncludes: false

The main consequences are:

Run ./format.sh before submitting any modification. This script only formats *.hxx, *.ixx and *.cxx files.

Static analysis and verification

Backward compatibility

The results given by the TFEL libraries and the assets generated by MFront are used in studies which must remain reproducible for many years. A development which does not fix an explicit bug, but changes the results or the behaviour of the TFEL libraries, the results of the assets generated by MFront or the behaviour of MFront itself, shall be handled by a compatibility mode. Typical examples of such developments are:

Fixing an explicit bug is not such a development: bugs are corrected for everybody, whatever the compatibility mode of the asset being generated is.

Such an evolution of the default behaviour shall only be introduced on a major version, and be gated by the compatibility mode associated with that version: the new behaviour is used by the assets which declare no compatibility mode, or a mode which is greater than, or equal to, this version; the previous behaviour is kept for the assets which declare an older mode. The future compatibility mode allows introducing such an evolution before the corresponding major version is released, so that it can be tested and anticipated.

Doing so implies that:

The mechanism itself is described in the pages devoted to compatibility modes in MFront and to compatibility modes in the TFEL libraries, whose last sections are the checklists of such a development.

Source files

Extensions and roles

Extension Role
.hxx interface: declarations, type aliases, enumerations, static constexpr members, function declarations
.ixx inline and template definitions, included at the bottom of the corresponding .hxx
.cxx non template definitions, static and anonymous namespace helpers
.h, .c reserved to the C interfaces (see C sources)

The .hxx/.ixx split is used when the implementation is long: the .hxx stays a readable interface and the .ixx holds the definitions. Both files have the same base name and both have their own include guard.

The .ixx file is included at the bottom of the .hxx, after the last namespace and before the closing #endif:

}  // end of namespace tfel::material

#include "TFEL/Material/Hosford1972YieldCriterion.ixx"

#endif /* LIB_TFEL_MATERIAL_HOSFORD_HXX */

A .cxx file shall never include a .ixx file.

Directory layout

Naming

A file is named after the main type it declares, in UpperCamelCase: ModellingHypothesis.hxx, BehaviourDescription.cxx.

Files whose name starts with a lowercase letter are the exception: they declare types mimicking the standard library (stensor.hxx, matrix.hxx, qt.hxx, power.hxx, runtime_array.hxx, …) or are executable entry points (main.cxx, mfront.cxx, mtest.cxx, mfm.cxx).

File header

Every source file and every header shall start by a doxygen comment describing the file and containing the copyright statement:

/*!
 * \file   include/TFEL/Material/BoundsCheck.hxx
 * \brief  This file declares the BoundsCheck class.
 * \author Thomas Helfer
 * \date   06/03/2007
 * \copyright Copyright (C) 2006-2025 CEA/DEN, EDF R&D. All rights
 * reserved.
 * This project is publicly released under either the GNU GPL Licence with
 * linking exception or the CECILL-A licence. A copy of thoses licences are
 * delivered with the sources of TFEL. CEA or EDF may also distribute this
 * project under specific licensing conditions.
 */

The tags are always in this order: \file, \brief, \author, \date, \copyright.

Code generated by MFront uses the same header, with the \copyright line replaced by the mention of the generator version.

Include guards

Headers are protected by an include guard built from the include path, in all upper case, ending with the extension of the file:

#ifndef LIB_TFEL_MATH_FORWARD_STENSOR_HXX
#define LIB_TFEL_MATH_FORWARD_STENSOR_HXX
...
#endif /* LIB_TFEL_MATH_FORWARD_STENSOR_HXX */

The closing #endif shall always repeat the name of the macro in a block comment. The same applies to every conditional compilation block: #endif /* NDEBUG */, #endif /* __cplusplus */, …

#pragma once is not used. .cxx files have no include guard.

Including headers

Order

The order of the #include directives is meaningful and shall be preserved:

  1. standard headers, in angle brackets,
  2. headers of third-party libraries, in quotes,
  3. TFEL headers,
  4. headers of the current library (MFront, MTest, …),
  5. the header implemented by the current .cxx file, last.

TFEL headers shall therefore appear after the standard headers and after the headers of the third-party libraries. A blank line separates the standard headers from the following groups; it is optional but recommended.

#include <cstring>
#include <sstream>
#include <iterator>
#include <algorithm>
#include <stdexcept>

#include "TFEL/Raise.hxx"
#include "TFEL/Glossary/Glossary.hxx"
#include "TFEL/Utilities/CxxTokenizer.hxx"
#include "MFront/MFrontLogStream.hxx"
#include "MFront/BehaviourData.hxx"

Since SortIncludes is disabled, adding a new include shall not trigger a reordering of the existing ones.

Syntax

Namespaces

Namespaces use the a::b::c syntax and are always closed by a comment giving their full name:

namespace tfel::material {

  ...

}  // end of namespace tfel::material

The main namespaces are tfel::math, tfel::material, tfel::system, tfel::tests, tfel::utilities, tfel::typetraits, mfront, mfront::bbrick, mtest, tfel::check and the namespaces of the solver interfaces (abaqus, aster, ansys, castem, calculix, dianafea, lsdyna, …).

Implementation details are put in a sub-namespace named internals or details.

using namespace is forbidden in headers. It is allowed in functions of .cxx files, where it is the usual way to shorten tests and implementations:

  tfel::tests::TestResult execute() override {
    using namespace tfel::material::moduli_symmetries;
    ...
  }  // end of execute

Naming conventions

Types

Types, enumerations and type aliases are in UpperCamelCase: ModellingHypothesis, BehaviourDescription, IsotropicModuli, StressStensor, TestResult.

Type aliases use using and never typedef. They are documented by the conventional comment //! a simple alias:

    //! a simple alias
    using size_type = std::vector<std::thread>::size_type;

Methods and free functions

Methods and free functions are written in camelCase, starting with a verb:

  static bool isModellingHypothesis(const std::string&);
  static std::string toString(const Hypothesis);
  [[nodiscard]] size_type getNumberOfThreads() const;
  TFEL_HOST_DEVICE constexpr StressType computeLambda(const StressType&);

The conventional prefixes are get/set, is/has, check, throw, compute, add/insert/remove, to.

Data members and variables

Data members and variables, including function parameters when they are not trivial, are written in snake_case:

    //! number of integration points
    const size_type nb_of_integration_points;
    //! name of the modelling hypothesis
    std::string modelling_hypothesis;
    //! lower bound
    double lower_bound = std::numeric_limits<double>::quiet_NaN();

Multi-word names use underscores, never internal capitals: cblock_begin, is_mutable, numerical_jacobian_epsilon, include_paths.

Single word names are preferred for short lived variables and for parameters whose meaning is given by the \param documentation: n, v, s, os, lb, ub, eps.

The historical sources use a mix of UpperCamelCase and camelCase for data members, without any prefix (_, m_ and s_ are never used). New code and modified code shall use snake_case; existing entities shall not be renamed outside of the scope of the current change.

Template parameters

Descriptive template parameters are in UpperCamelCase (StressStensor, NumericType, InputIterator, ValueType), generic ones use a single letter or a short list (T, U, F, Op, T1, T2). Boolean and concept parameters use lowercase (real, stress, use_qt).

Enumerations

Enumerations are in UpperCamelCase, their enumerators in ALL_UPPER_CASE. Each enumerator is documented by a trailing //<! comment and the enumeration is closed by a // end of enum comment:

  /*!
   * \brief list the possible values for the logging facilities
   *        provided by the getVerboseMode/getLogStream functions
   */
  enum VerboseLevel {
    VERBOSE_QUIET = -1,  //<! no output
    VERBOSE_LEVEL0 = 0,  //<! minimal output
    VERBOSE_LEVEL1 = 1,  //<! a simpler output
    VERBOSE_LEVEL2 = 2,  //<! a much detailled output
    VERBOSE_LEVEL3 = 3,  //<! the finer level for standard user
    VERBOSE_DEBUG = 4,   //<! an output adapted for debugging
    VERBOSE_FULL = 5     //<! a very detailled output
  };                     // end of enum VerboseLevel

A plain enum is preferred to an enum class when the enumerators are used as array indices or have to be visible in the C or Fortran interfaces; an enum class is used otherwise.

Macros and constants

Macros are in ALL_UPPER_CASE and prefixed by the library name (TFEL_, MFRONT_, MTEST_, …). They are documented with a \def block:

/*!
 * \def TFEL_PP_JOIN
 * \brief this macro joins its two arguments together.
 * \author Thomas Helfer
 * \date 28/08/2006
 */
#define TFEL_PP_JOIN(X, Y) TFEL_PP_DO_JOIN(X, Y)

static constexpr constants use a descriptive UpperCamelCase name and, when the constant is a well known physical quantity, a short lowercase alias (see TFEL/PhysicalConstants.hxx).

Structures and classes

struct shall be used in place of class: struct is used more than 3 000 times in the sources, whereas class is essentially reserved to template parameters.

Public members are declared first. Since struct is already public by default, most types have no public: label at all: the private section is written at the end of the type.

The conventional order of the members of a type is:

  1. documentation of the type,
  2. using aliases,
  3. nested types and enumerations,
  4. static constexpr constants and static string constants,
  5. constructors, destructor, copy and move operators,
  6. methods,
  7. protected: section, if any,
  8. private: section, if any, with private methods first and data members last.
  /*!
   * \brief structure handling a fixed-size pool of threads
   */
  struct TFELSYSTEM_VISIBILITY_EXPORT ThreadPool {
    //! a simple alias
    using size_type = std::vector<std::thread>::size_type;
    /*!
     * \brief constructor
     * \param[in] n: number of thread to be created
     */
    explicit ThreadPool(const size_type);
    ...
    //! \return the number of threads managed by the pool
    [[nodiscard]] size_type getNumberOfThreads() const;
    //! destructor
    ~ThreadPool();

   private:
    //! enum describing the status of workers
    enum Status { WORKING, IDLE };  // end of enum Status
    //! status of the workers
    std::vector<Status> statuses;
    //! list of available threads
    std::vector<std::thread> workers;
    // the task queue
    std::queue<std::function<void()>> tasks;
  };  // end of struct ThreadPool

Non-copyable types explicitly disable the special members rather than omitting them:

    //! \brief copy constructor (disabled)
    TestCase(const TestCase&) = delete;
    //! \brief assignement operator (disabled)
    TestCase& operator=(const TestCase&) = delete;

Leaf types are declared final.

The this pointer

Data members and methods shall always be qualified by this->, even when the qualification is not required:

  void LogarithmicStrainHandlerBase::checkLagrangianSetting() const {
    raise_if(this->s != LAGRANGIAN,
             "LogarithmicStrainHandlerBase::"
             "checkLagrangianSetting: "
             "setting is not set to LAGRANGIAN");
  }  // end of LogarithmicStrainHandlerBase::checkLagrangianSetting

This convention makes member accesses immediately visible, avoids shadowing bugs, and is mandatory in template classes, where the this->template form is required to access dependent members:

    this->template test_1<float, false>();
    this->template computeEigenValues<es>(vp0, vp1, vp2, this->v, b);

Documentation

General rules

All members, functions and methods shall be documented with doxygen, in the .hxx file: the doxygen configuration (Doxyfile.in) only processes *.hxx, *.ixx and *.doxygen files, so a .cxx file is never documented.

Documentation is written in C++ comments, using backslash commands (\brief, never @brief):

/// and /** ... */ are not used.

Conventional forms

  /*!
   * \brief compute the Hosford yield stress
   * \tparam StressStensor: type of the stress tensor
   * \tparam HosfordExponentType: type of the Hosford exponent
   * (could be a numeric type or an integer type)
   * \param[in] s: stress tensor
   * \param[in] a: Hosford exponent
   * \param[in] e: criterion used to check if the stress are null
   */
  template <tfel::math::StensorConcept StressStensor,
            typename HosfordExponentType>
  TFEL_HOST_DEVICE HosfordStressType<StressStensor> computeHosfordStress(
      const StressStensor&,
      const HosfordExponentType,
      const HosfordStressType<StressStensor>);

The following one line comments are conventional and shall be used consistently:

    //! a simple alias
    //! \brief default constructor
    //! \brief copy constructor (disabled)
    //! \brief assignement operator (disabled)
    //! destructor
    //! \return the name of the class

Documentation is also expected for private members, private methods and private data members, whose comment states their role.

Language conventions

Constness

const is written before the type and is applied to parameters even when they are passed by value, in the declarations as well as in the definitions:

  static std::string toString(const Hypothesis);
  void checkModellingHypothesis(const Hypothesis) const;

Local variables are const by default: const auto is far more frequent than auto, and for (const auto& x : ...) far more frequent than for (auto& x : ...).

Integer types

Variables, members and parameters are not declared long or long long, unless the API of a third-party library requires it. For instance, the 64 bits atomic operations of CUDA only accept unsigned long long. Such a type is then hidden behind an alias, documented with the reason.

Attributes

Attributes come before the visibility macro and before the return type: [[nodiscard]] TFEL_HOST_DEVICE ....

Closing comments

Namespaces, types, functions and enumerations are closed by a comment giving their name, with two spaces before the comment:

  }  // end of computeIsotropicStiffnessTensor
};  // end of struct BehaviourData
}  // end of namespace mfront
}  // end of enum VerboseLevel

Control flow blocks (if, for, while) are not annotated.

Exported symbols

A type or a function belonging to the public interface of a library is annotated with the visibility macro of the library, <LIBRARY>_VISIBILITY_EXPORT, where the library name is the name of the CMake target (TFELMATH_VISIBILITY_EXPORT, TFELMATERIAL_VISIBILITY_EXPORT, TFELTESTS_VISIBILITY_EXPORT, MFRONT_VISIBILITY_EXPORT, …). Such macros are defined in TFEL/Config/TFELConfig.hxx for the TFEL libraries and in MFront/MFrontConfig.hxx for the MFront libraries. The macro is written immediately before struct, or before the return type of a function, after the attributes.

TFEL_VISIBILITY_LOCAL is used for helpers which are defined in headers but must not be exported.

Templates are not exported: their definitions are in the .ixx files and the non template helpers they use are marked TFEL_VISIBILITY_LOCAL.

Portability

Every function which must be usable on a GPU or through a SYCL backend is annotated with TFEL_HOST_DEVICE (or TFEL_HOST, TFEL_DEVICE), written before the return type and before constexpr (see TFEL/Config/TFELConfig.hxx).

The same restriction applies to the code of such functions: no allocation, no exception, no STL container which is not supported by the targeted backends.

TFEL_INLINE and its variants are used instead of inline when the inlining is performance critical.

Error handling

Error handling in TFEL/Math and TFEL/Material

Due to GPU compatibility, usage of exceptions in TFEL/Math and TFEL/Material is generally prohibited. There are exceptions for parts not meant to ported on the GPU, notably TFEL/Math/Parser. For historical reasons and backward compatibility, some usage of exceptions may still exist in other parts of those libraries.

As a general rule, one shall use the return type to indicate success or failure: boolean values (true for success, false for failuer), error codes, etc..

Error handling in other parts of the project

There is no error reporting macro. Errors are reported by calling functions declared in TFEL/Raise.hxx:

  template <typename Exception = std::runtime_error>
  [[noreturn]] TFEL_VISIBILITY_LOCAL TFEL_INLINE void raise();

  template <typename Exception = std::runtime_error, typename... Args>
  [[noreturn]] TFEL_VISIBILITY_LOCAL TFEL_INLINE void raise(Args&&...);

  template <typename Exception = std::runtime_error, typename... Args>
  TFEL_VISIBILITY_LOCAL TFEL_INLINE void raise_if(const bool, Args&&...);

The exception object is built outside of the throw statement, so that the cert-err60-cpp rule is satisfied even if the constructor of the exception throws.

Error messages are written as <Class>::<method>: <message>. They are split into adjacent string literals to respect the 80 columns limit, and completed by string concatenation when they contain dynamic parts:

    tfel::raise_if(this->behaviour.empty(),
                   "BehaviourDescription::getBehaviourName: "
                   "behaviour name not defined");
    ...
    tfel::raise("BehaviourData::CodeBlocksAggregator::set: " + m);

Exception classes are struct types, exported and derived from TFELException (TFEL/Exception/TFELException.hxx), MaterialException (TFEL/Material/MaterialException.hxx), MathRunTimeException, SystemError, … according to the library they belong to.

Out-of-bounds checks are handled by the policy classes of TFEL/Material/BoundsCheck.hxx, which use the Warning, Strict or None policies of TFEL/Material/OutOfBoundsPolicy.hxx.

Dynamic polymorphism

Dynamic polymorphism in TFEL/Math and TFEL/Material

Due to GPU compatibility, usage of dynamic polymorphism in TFEL/Math and TFEL/Material is generally prohibited. There are exceptions for parts not meant to ported on the GPU, notably TFEL/Math/Parser.

Static polymorphism, through CRTP (Curiously Recurring Template Pattern, see [1]), is used in many places as a substitute for dynamic polymorphism.

Dynamic polymorphism in other parts of the project

Dynamic polymorphism is used extensively in other parts of the project. The general scheme is as follows:

Multiple inheritance combined with dynamic polymorphism is discouraged.

Factories

A hierarchy whose instances are designated by a name, given in an input file, on the command line or by a script, is associated with a factory. The factory is declared in <Name>Factory.hxx, in the same namespace as the abstract type, and named after the type it creates: DSLFactory, BehaviourInterfaceFactory, ModelInterfaceFactory, MaterialPropertyInterfaceFactory, NonLinearSystemSolverFactory, BehaviourBrickFactory, bbrick::InelasticFlowFactory, bbrick::IsotropicHardeningRuleFactory, bbrick::StressCriterionFactory, bbrick::StressPotentialFactory, bbrick::PorosityNucleationModelFactory, bbrick::StrainRateSensitivityFactorFactory, generic_parallel::material_property::BackendFactory, …

All those factories follow the same pattern:

A factory may be both coarse grained and extensible from the outside: BehaviourBrickFactory is the single factory of all the behaviour bricks, its constructor registers the generic bricks, and the DSLs register the bricks they own with registerAbstractBehaviourBrick.

  //! \brief abstract factory for isotropic hardening rules.
  struct MFRONT_VISIBILITY_EXPORT IsotropicHardeningRuleFactory {
    //! \brief a simple alias
    using Generator = std::function<std::shared_ptr<IsotropicHardeningRule>()>;
    //! \return the uniq instance of the class
    [[nodiscard]] static IsotropicHardeningRuleFactory& getFactory();
    //
    IsotropicHardeningRuleFactory(IsotropicHardeningRuleFactory&&) = delete;
    IsotropicHardeningRuleFactory(const IsotropicHardeningRuleFactory&) =
        delete;
    IsotropicHardeningRuleFactory& operator=(IsotropicHardeningRuleFactory&&) =
        delete;
    IsotropicHardeningRuleFactory& operator=(
        const IsotropicHardeningRuleFactory&) = delete;
    //! \brief return the list of available isotropic hardening rules
    [[nodiscard]] std::vector<std::string> getRegistredIsotropicHardeningRules()
        const;
    /*!
     * \brief add a new generator
     * \param[in] n: name of the generator
     * \param[in] g: generator
     */
    void addGenerator(const std::string&, const Generator&);
    /*!
     * \brief generate a new isotropic hardening rule
     * \param[in] n: name of the isotropic hardening rule
     */
    [[nodiscard]] std::shared_ptr<IsotropicHardeningRule> generate(
        const std::string&) const;

   private:
    //! \brief default constructor
    IsotropicHardeningRuleFactory();
    //! \brief destructor
    ~IsotropicHardeningRuleFactory();
    //! \brief generators
    std::map<std::string, Generator> generators;
  };  // end of struct IsotropicHardeningRuleFactory

The implementation of the singleton, of the two methods which manipulate the map and of the constructor is fixed:

  IsotropicHardeningRuleFactory& IsotropicHardeningRuleFactory::getFactory() {
    static IsotropicHardeningRuleFactory i;
    return i;
  }  // end of getFactory

  void IsotropicHardeningRuleFactory::addGenerator(const std::string& n,
                                                   const Generator& g) {
    if (!this->generators.insert({n, g}).second) {
      tfel::raise(
          "IsotropicHardeningRuleFactory::addGenerator: "
          "generator '" +
          n + "' already registred");
    }
  }  // end of addGenerator

  std::shared_ptr<IsotropicHardeningRule>
  IsotropicHardeningRuleFactory::generate(const std::string& n) const {
    const auto p = this->generators.find(n);
    if (p == this->generators.end()) {
      tfel::raise(
          "IsotropicHardeningRuleFactory::generate: "
          "no generator named '" +
          n + "'");
    }
    return p->second();
  }  // end of generate

  IsotropicHardeningRuleFactory::IsotropicHardeningRuleFactory() {
    this->addGenerator("Linear", []() {
      return std::make_shared<bbrick::LinearIsotropicHardeningRule>();
    });
    this->addGenerator("Voce", []() {
      return std::make_shared<bbrick::VoceIsotropicHardeningRule>();
    });
    ...
  }  // end of IsotropicHardeningRuleFactory

A factory may store more information than a single generator: DSLFactory stores the aliases and the description of each DSL, BehaviourBrickFactory stores the behaviour types and the integration schemes for which a brick is valid, the interface factories store the dependencies of an interface with another one. Those additional properties are stored in maps keyed by the same name.

Proxies

A factory is fed by proxies, small template structures whose only role is to build the generator of a given type and to register it: DSLProxy, BehaviourInterfaceProxy, ModelInterfaceProxy, MaterialPropertyInterfaceProxy and, in the unit tests framework, tfel::tests::TestProxy, which is instantiated by the TFEL_TESTS_GENERATE_PROXY macro.

A proxy is declared in <Name>Proxy.hxx, is not exported, since it is a header only template, and the definitions of its members are written in a <Name>Proxy.ixx file included at the end of the header:

  /*!
   * \brief structure used to register a DSL generator
   * \tparam DSLType: type of the DSL generated
   */
  template <typename DSLType>
  struct DSLProxy {
    //! \brief default constructor
    DSLProxy();
    /*!
     * \brief constructor
     * \param[in] aliases: alternative names for the DSL
     */
    explicit DSLProxy(const std::vector<std::string>&);
    /*!
     * \brief generate an instance of the DSL
     * \param[in] opts: options passed to the DSL constructor
     */
    static std::shared_ptr<AbstractDSL> createDSL(
        const AbstractDSL::DSLOptions&);
  };  // end of struct DSLProxy

}  // end of namespace mfront

#include "MFront/DSLProxy.ixx"

The constructor of the proxy registers the generator built by the static member createDSL, which is no more than a std::make_shared of the template parameter. The name of the type is given by a static getName method of the registered type itself, and the overloaded constructors are used to declare the aliases and the dependencies of this type:

  template <typename DSLType>
  DSLProxy<DSLType>::DSLProxy() {
    auto& f = DSLFactory::getDSLFactory();
    f.registerDSLCreator(DSLType::getName(), &DSLProxy<DSLType>::createDSL,
                         &DSLType::getDescription);
  }  // end of DSLProxy

  template <typename DSLType>
  std::shared_ptr<AbstractDSL> DSLProxy<DSLType>::createDSL(
      const AbstractDSL::DSLOptions& opts) {
    const auto& opts_validator = DSLType::getDSLOptionsValidator();
    opts_validator.validate(opts);
    return std::make_shared<DSLType>(opts);
  }  // end of createDSL

A proxy is a plain object, which must be constructed for the registration to take place. Proxies are created as local objects of an explicit initialisation function, initDSLs() or initInterfaces(), which the executables shall call before using the factory and which shall be idempotent and thread safe:

  void initDSLs() {
    static bool init = false;
    ...
    if (init) {
      return;
    }
    if (!init) {
      DSLProxy<DefaultDSL> proxy1(
          std::vector<std::string>{"Default", "DefaultParser"});
      DSLProxy<IsotropicMisesCreepDSL> proxy2("IsotropicMisesCreepParser");
      ...
      init = true;
    }
  }  // end of initDSLs

A global proxy is only used in the interface libraries which are not built in MFront and are loaded on demand, since the main program cannot know the types they declare:

  ModelInterfaceProxy<MFrontGerminalModelInterface> ModelGerminalProxy;

A proxy is not necessarily one of the templates above: a type registered by a library external to MFront is registered by a local structure with a static instance:

  struct MyStressCriterionProxy {
    MyStressCriterionProxy() {
      auto& f = StressCriterionFactory::getFactory();
      f.addGenerator("MyStressCriterion", []() {
        return std::make_shared<MyStressCriterion>();
      });
    }  // end of MyStressCriterionProxy
  };  // end of struct MyStressCriterionProxy

  static MyStressCriterionProxy proxy;

In short, adding a new type to an existing hierarchy amounts to:

  1. deriving from the abstract type, or from the Base class which provides the default implementation of its interface,
  2. giving the new type a static getName method, which is the name used by the proxy to register it,
  3. registering a generator in the corresponding factory, either from the constructor of the factory itself (types which are always available), from a proxy created by an initialisation function (interfaces and DSLs), or from a local proxy with a static instance (types of an external library).

Unit tests

Unit tests are written with the framework of TFEL/Tests, which is documented here. A test file is a .cxx file named after the tested entity, located in the tests directory of the library, and follows this layout:

#ifdef NDEBUG
#undef NDEBUG
#endif /* NDEBUG */

#include <limits>
#include <cstdlib>
#include <iostream>
#include "TFEL/Config/TFELTypes.hxx"
#include "TFEL/Material/Lame.hxx"
#include "TFEL/Tests/TestCase.hxx"
#include "TFEL/Tests/TestProxy.hxx"
#include "TFEL/Tests/TestManager.hxx"

template <typename T>
static constexpr T my_abs(const T& v) noexcept {
  return v < T(0) ? -v : v;
}

struct LameTest final : public tfel::tests::TestCase {
  LameTest()
      : tfel::tests::TestCase("TFEL/Material", "Lame") {}  // end of LameTest
  tfel::tests::TestResult execute() override {
    this->template test1<float, false>();
    this->template test1<double, false>();
    this->template test1<long double, false>();
    return this->result;
  }  // end of execute

 private:
  //! check the computation of the Lame coefficients
  template <typename NumericType, bool use_qt>
  void test1() {
    using stress =
        typename tfel::config::Types<1u, NumericType, use_qt>::stress;
    using real = typename tfel::config::Types<1u, NumericType, use_qt>::real;
    constexpr auto eps = 10 * std::numeric_limits<stress>::epsilon();
    constexpr auto young = stress{150e9};
    constexpr auto nu = real{0.3};
    constexpr auto l = tfel::material::computeLambda(young, nu);
    constexpr auto l_ref = young * nu / ((1 + nu) * (1 - 2 * nu));
    TFEL_TESTS_STATIC_ASSERT(my_abs(l - l_ref) < eps);
  }  // end of test1
};  // end of struct LameTest

TFEL_TESTS_GENERATE_PROXY(LameTest, "Lame");

/* coverity [UNCAUGHT_EXCEPT]*/
int main() {
  auto& m = tfel::tests::TestManager::getTestManager();
  m.addTestOutput(std::cout);
  m.addXMLTestOutput("Lame.xml");
  return m.execute().success() ? EXIT_SUCCESS : EXIT_FAILURE;
}  // end of main

The points to note are:

TFEL_TESTS_FUNCTION is a legacy interface and shall not be used in new tests.

C sources

C sources are limited to the interfaces which must be C compatible: the GenericBehaviour interface, the getFunction helpers, the ProcessManager helpers.

The same file header, the same include guards and the same documentation rules apply. The differences are:

format.sh does not format .c and .h files: they shall be formatted as if clang-format had been run on them.

Build system

The conventions of the build system are as important as the conventions of the sources, since a new file which is not declared is not a part of the library.

Documentation

The website is built from the markdown files of docs/web by pandoc. A new page is added by calling pandoc_html in docs/web/CMakeLists.txt and by adding an entry in the menu of docs/web/mfront-template.html.

The markdown conventions of the project are:

The doxygen documentation is generated separately from the headers (see the doxygen workflow of the continuous integration).

Checklist before submitting

Before submitting a pull request, or a patch generated by a coding agent, check that:

  1. the file header of every new file contains the doxygen description of the file and the copyright statement,
  2. the headers of the file are ordered as specified (standard headers, then third-party headers, then TFEL headers, then the headers of the current library, then the own header),
  3. the file is named after the main type it declares,
  4. types are struct, public members are declared first,
  5. data members and variables are in snake_case, methods and free functions are in camelCase,
  6. data members and methods are qualified by this->,
  7. every member, function and method is documented, in the header,
  8. namespaces, types, functions and enumerations are closed by a // end of ... comment,
  9. ./format.sh has been run,
  10. new sources and new tests are declared in the CMakeLists.txt files,
  11. a development which does not fix an explicit bug but changes the results or the behaviour is handled by a compatibility mode, and both behaviours are tested,
  12. ./check_all.sh -f passes,
  13. new features are described in the relevant release notes file of docs/web/release-notes,
  14. new pages are declared in docs/web/CMakeLists.txt and added to the menu of docs/web/mfront-template.html.
1.
Coplien, James O. Curiously recurring template patterns. C++ Report. 1995. Vol. 7, no. 2, p. 24–27.