Skip to main content

Library for Python generation of C++ customization point objects with tag_invoke

Project description

TInCuP - Tag Invoke + Customization Points

TInCuP Logo

CI OpenSSF Best Practices OpenSSF Scorecard PyPI Python Versions CMake Meson VS%20Code Vim CLion GCC Clang MSVC

TInCuP helps you add clean, extensible “hooks” to your C++ library. Describe what you want in a simple spec, generate correct code, and get readable compiler messages when something goes wrong.

Why Should I Care About Customization Points (and TInCuP?)

Have you ever been in a situation like the following?

  • You are developing ImpressiveLib, a very impressive library.
  • ImpressiveLib uses AwesomeType, which is defined in CoolLib, an open source library that you don't own.
  • You'd really like to use SpiffyLib, another library you don't own, to do something with AwesomeType.
  • Unfortunately there is an interface incompatibility. It looks like you need to modify one of the two libraries. (DON'T!)
  • This is where customization points can help!

"BUT!" you say, "I don't have a three body problem. I don't care about SpiffyLib, I just want to use AwesomeType with ImpressiveLib without making CoolLib a requirement."

Well, that's essentially the same problem, AKA "The Library Design Problem." It's technically only a two body problem, but you are asking for ImpressiveLib to be generic and allow non-intrusive customization. This is what std::ranges does and customization point objects are how it does it.

Installation

  • Python tools: pip install TInCuP==1.0.0 (installs the cpo-generator CLI)
  • From source (development): pip install -e .

Troubleshooting CLI on macOS/Linux:

  • If cpo-generator is not found after install, ensure your user scripts directory is on PATH:
    • export PATH="$(python3 -m site --user-base)/bin:$PATH"
    • Add the line above to your shell profile (e.g., ~/.zshrc or ~/.bashrc).
  • Alternatively, use pipx to manage CLI tools: pipx install TInCuP==1.0.0
  • The module form always works: python3 -m cpo_tools.cpo_generator --help.

Get, Feedback, Contribute

Table of Contents

The Problem with Existing Approaches

C++ customization mechanisms have fundamental limitations that the standardization committee has recognized in WG21 papers P1895R0 and P2279R0:

  • Namespace Pollution: Traditional ADL customization requires globally reserving function names
  • Composition Issues: Multiple libraries can't safely use the same customization point names
  • Boilerplate Complexity: The tag_invoke pattern, while solving namespace issues, requires significant repetitive code

TInCuP's Solution

TInCuP bridges the gap between tag_invoke's theoretical benefits and practical usability:

Eliminates Boilerplate - Automated code generation from simple JSON specifications
Enforces Consistency - Pattern verification ensures uniform implementations
Developer Experience - Comprehensive IDE integrations make invisible interfaces visible
Static Dispatch Integration - Compile-time optimization for runtime configuration choices
Future-Proof - Standardized patterns enable automated refactoring as the language evolves

Bridge Technology: TInCuP makes tag_invoke practical today while C++ evolves toward better built-in solutions.

Static Dispatch Integration

TInCuP includes compile-time dispatch utilities that turn runtime choices into template specializations — preserving performance while keeping a simple API. Full guide.

  • Zero-overhead after dispatch; unlocks compiler optimizations
  • Works with boolean and string dispatch patterns
Boolean Dispatch example

Generate a CPO that branches on a runtime boolean with compile-time optimization:

cpo-generator '{"cpo_name": "choose_path", 
                "args": ["$T&: input"], 
                "runtime_dispatch": {
                  "type": "bool", 
                  "dispatch_arg": "selector", 
                  "options": ["yin", "yang"]}}'

Generated CPO and usage:

inline constexpr struct choose_path_ftor final : tincup::cpo_base<choose_path_ftor> {
  TINCUP_CPO_TAG("choose_path")
  
  struct yin_tag {};
  struct yang_tag {};

  template<typename T>
  constexpr auto operator()(T& input, bool selector = false) const {
    tincup::BoolDispatch dispatcher(selector);
    return dispatcher.receive([&](auto dispatch_constant) {
      if constexpr (dispatch_constant.value) {
        return tag_invoke(*this, input, yin_tag{});
      } else {
        return tag_invoke(*this, input, yang_tag{});
      }
    });
  }

  template<typename T>
  constexpr auto operator()(T& input, yin_tag) const {
    return tag_invoke(*this, input, yin_tag{});
  }
  
  template<typename T>
  constexpr auto operator()(T& input, yang_tag) const {
    return tag_invoke(*this, input, yang_tag{});
  }
} choose_path;

// Both interfaces available
auto result1 = choose_path(data, runtime_flag);    // Runtime -> compile-time
auto result2 = choose_path(data, choose_path_ftor::yin_tag{});  // Direct compile-time
String Dispatch example

Generate a CPO that dispatches on runtime string values with compile-time specialization:

cpo-generator '{"cpo_name": "execute_policy", 
                "args": ["$T&: data"], 
                "runtime_dispatch": {
                  "type": "string", 
                  "dispatch_arg": "policy_name", 
                  "options": ["fast", "safe", "debug"]}}'

Generated CPO and usage:

inline constexpr struct execute_policy_ftor final : tincup::cpo_base<execute_policy_ftor> {
  TINCUP_CPO_TAG("execute_policy")
  
  struct fast_tag {};
  struct safe_tag {};
  struct debug_tag {};
  struct not_found_tag {};  // For unrecognized strings

  template<typename T>
  constexpr auto operator()(T& data, std::string_view policy_name) const {
    static constexpr std::array<std::string_view, 3> options = {
      "fast", "safe", "debug"
    };
    tincup::StringDispatch<3> dispatcher(policy_name, options);
    return dispatcher.receive([&](auto dispatch_constant) {
      if constexpr (dispatch_constant.value == 0) {
        return tag_invoke(*this, data, fast_tag{});
      } else if constexpr (dispatch_constant.value == 1) {
        return tag_invoke(*this, data, safe_tag{});
      } else if constexpr (dispatch_constant.value == 2) {
        return tag_invoke(*this, data, debug_tag{});
      } else {
        return tag_invoke(*this, data, not_found_tag{});
      }
    });
  }
  
  // Direct compile-time dispatch overloads also available...
} execute_policy;

// Runtime string dispatches to compile-time specializations
auto result = execute_policy(data, "fast");

Enhanced Error Diagnostics

Concise, actionable compiler errors for common CPO misuse (pointers, const-qualification, argument order/arity) with C++23/C++26 enhancements and opt-out controls for compile-time sensitive builds.

  • Quick benefits: educational messages, zero runtime cost, bounded compile-time overhead
  • Controls: TINCUP_DIAGNOSTIC_LEVEL and category toggles
  • Full guide: see Diagnostics
Example diagnostic (pointer/smart pointer)
std::unique_ptr<Vector> vec_ptr = std::make_unique<Vector>();
add_assign(vec_ptr, other_vec);  // ❌ pointer instead of object

Message:

CPO: No valid tag_invoke overload, but a valid overload exists for dereferenced arguments.
Consider: add_assign(*vec_ptr, other_vec)

Features

  • Header-Only C++20 Library: A single header <tincup/tincup.hpp> provides all necessary components.
  • Static Dispatch Integration: Built-in compile-time dispatch utilities (BoolDispatch, StringDispatch) that convert runtime configuration into zero-overhead template specializations.
  • Enhanced Error Diagnostics: Comprehensive misuse detection system catching 5 major categories of CPO mistakes with helpful, educational error messages (even more sophisticated with C++23). Performance opt-outs available.
  • CMake and Meson Support: First-class integration with both build systems for easy inclusion in any project.
  • Powerful Code Generator: A Python-based command-line tool, cpo-generator, to automatically generate CPO boilerplate from simple JSON definitions, including static dispatch-enabled CPOs.
  • IDE/Editor Integration: Full support for Vim, VSCode, and CLion with plugins, templates, and external tools.
  • Modern & Friendly: Designed with modern C++ and Python practices to be user-friendly and easy to extend.

C++ Library Usage

This is a header-only library. You can use it in two ways:

Option 1: Multi-Header (Default)

#include <tincup/tincup.hpp>

Option 2: Single Header (Convenience)

For easier distribution or simple integration, use the single header version:

#include "single_include/tincup.hpp"  // All functionality in one file

The single header is automatically generated from the multi-header version and contains:

  • All TInCuP functionality consolidated into one file
  • Proper copyright banners and organized includes
  • Same API and performance as the multi-header version

Generate the latest single header: cd scripts && python generate_single_header.py

Quick Start

New to the project? See Getting started for a complete guide.

Documentation

Integration with Build Systems

CMake

Standard FetchContent integration works seamlessly:

FetchContent_Declare(tincup
  GIT_REPOSITORY https://github.com/sandialabs/TInCuP.git)
FetchContent_MakeAvailable(tincup)
target_link_libraries(your_target PRIVATE tincup::tincup)

See Build Systems/cmake/README.md for advanced usage and docs/BUILD_SYSTEMS.md for integration details.

Meson

See build_systems/meson/README.md for detailed Meson integration.

Quick example:

tincup_dep = dependency('tincup', fallback : ['tincup', 'tincup_dep'])
executable('my_exe', 'main.cpp', dependencies : [tincup_dep])

Code Generation Tool (cpo-generator)

The Python-based code generator is the fastest way to create new CPOs.

Installation

Preferred (PyPI):

pip install TInCuP==1.0.0

For development (from source):

pip install -e .

Generator Syntax

The generator uses a compact JSON syntax to define CPOs. The args array takes a list of strings, where each string is a type: name pair.

To indicate a template type, prefix the type with a $ symbol. This will be expanded to typename in the generated C++ code.

"$T&" becomes template<typename T> ... (T&)

Argument DSL

Use a compact DSL in the args array: each entry is type: name. Types can be concrete (e.g., int, double&) or generic using $.

Token/Pattern Meaning Example input Generated signature fragment
$T Generic by value "$T: x" template<typename T>(T x)
$T& Generic lvalue reference "$T&: x" template<typename T>(T& x)
$T&& Forwarding reference "$T&&: x" template<typename T>(T&& x) (forwarded)
$T... Generic parameter pack (by value) "$T...: xs" template<typename... T>(T... xs)
$T&... Generic lvalue reference pack "$T&...: xs" template<typename... T>(T&... xs)
$T&&... Forwarding reference pack "$T&&...: xs" template<typename... T>(T&&... xs) (forwarded)
$const T Const-qualified generic "$const T: x" template<typename T>(const T x)
$const T& Const lvalue reference "$const T&: x" template<typename T>(const T& x)
$volatile T& Volatile lvalue reference "$volatile T&: x" template<typename T>(volatile T& x)
Concrete Concrete type (value/ref/rvalue) "int: n", "std::string&&: s" int n, std::string&& s

Notes:

  • Forwarding references are invoked with perfect forwarding, e.g., std::forward<T>(x).
  • const/volatile qualifiers are preserved in parameter types; volatile is supported but hasn’t been extensively tested yet.
  • Types and parameter names are separated by :. Types may include spaces/qualifiers.

Usage

The tool can be invoked from the command line with a JSON string. Run cpo-generator --help for a full list of options and examples.

# Generate a CPO with Doxygen comments
cpo-generator '{"cpo_name": "my_cpo", "args": ["$V&: x"]}' --doxygen

# Output to a header with include and namespace
cpo-generator '{"cpo_name": "my_cpo", "args": ["$V&: x"]}' \
  --with-include --namespace mylib --out include/mylib/my_cpo.hpp

# Generate a CPO with boolean static dispatch
cpo-generator '{"cpo_name": "choose_path", 
                "args": ["$T&: input"], 
                "runtime_dispatch": {
                  "type": "bool", 
                  "dispatch_arg": "selector", 
                  "options": ["yin", "yang"]}}'

# Generate a CPO with string static dispatch
cpo-generator '{"cpo_name": "execute_policy", 
                "args": ["$T&: data"], 
                "runtime_dispatch": {
                  "type": "string", 
                  "dispatch_arg": "policy_name", 
                  "options": ["fast", "safe", "debug"]}}'

# List LLM-friendly operation patterns
cpo-generator --llm-help

Examples

Generic CPO

A basic CPO with generic template parameters

Command:

cpo-generator {"cpo_name": "generic_cpo", "args": ["$T1&: arg1", "$T2&: arg2"]}

Generated Code:

inline constexpr struct generic_cpo_ftor final : tincup::cpo_base<generic_cpo_ftor> {
  TINCUP_CPO_TAG("generic_cpo")
  inline static constexpr bool is_variadic = false;
    // Typed operator() overload - positive case only (generic)
  // Negative cases handled by tagged fallback in cpo_base
  template<typename T1, typename T2>
    requires tincup::invocable_c<generic_cpo_ftor, T1&, T2&>
  constexpr auto operator()(T1& arg1, T2& arg2) const
    noexcept(tincup::nothrow_invocable_c<generic_cpo_ftor, T1&, T2&>) 
    -> tincup::invocable_t<generic_cpo_ftor, T1&, T2&> {
    return tincup::tag_invoke_cpo(*this, arg1, arg2);
  }
} generic_cpo;

// Note: operator() methods are provided by cpo_base

// CPO-specific concepts and type aliases for convenient usage
template<typename T1, typename T2>
concept generic_cpo_invocable_c = tincup::invocable_c<generic_cpo_ftor, T1&, T2&>;

template<typename T1, typename T2>
concept generic_cpo_nothrow_invocable_c = tincup::nothrow_invocable_c<generic_cpo_ftor, T1&, T2&>;


template<typename T1, typename T2>
using generic_cpo_return_t = tincup::invocable_t<generic_cpo_ftor, T1&, T2&>;

template<typename T1, typename T2>
using generic_cpo_traits = tincup::cpo_traits<generic_cpo_ftor, T1&, T2&>;

// Usage: tincup::is_invocable_v<generic_cpo_ftor, T1&, T2&>
Concrete CPO

A CPO with concrete type parameters

Command:

cpo-generator {"cpo_name": "concrete_cpo", "args": ["int: value", "double&: ref"]}

Generated Code:

[[deprecated("CPO with all concrete types - consider using regular function instead. Intended for experimentation/testing only.")]]
inline constexpr struct concrete_cpo_ftor final : tincup::cpo_base<concrete_cpo_ftor> {
  TINCUP_CPO_TAG("concrete_cpo")
  inline static constexpr bool is_variadic = false;

  // Typed operator() overload - positive case only (concrete)  
  // Negative cases handled by tagged fallback in cpo_base
  constexpr auto operator()(int value, double& ref) const
    noexcept(noexcept(tincup::tag_invoke_cpo(*this, value, ref))) 
    -> decltype(tincup::tag_invoke_cpo(*this, value, ref)) {
    return tincup::tag_invoke_cpo(*this, value, ref);
  }
} concrete_cpo;

// Note: operator() methods are provided by cpo_base

// CPO-specific type aliases for convenient usage (concrete types)
// Note: No concept aliases for concrete types - types are already known
using concrete_cpo_return_t = tincup::invocable_t<concrete_cpo_ftor, int, double&>;
using concrete_cpo_traits = tincup::cpo_traits<concrete_cpo_ftor, int, double&>;

// Usage: tincup::is_invocable_v<concrete_cpo_ftor, int, double&>
Forwarding Reference

A CPO using perfect forwarding for universal references

Command:

cpo-generator {"cpo_name": "forwarding_ref_cpo", "args": ["$T&&: fwd_ref"]}

Generated Code:

inline constexpr struct forwarding_ref_cpo_ftor final : tincup::cpo_base<forwarding_ref_cpo_ftor> {
  TINCUP_CPO_TAG("forwarding_ref_cpo")
  inline static constexpr bool is_variadic = false;
    // Typed operator() overload - positive case only (generic)
  // Negative cases handled by tagged fallback in cpo_base
  template<typename T>
    requires tincup::invocable_c<forwarding_ref_cpo_ftor, T>
  constexpr auto operator()(T&& fwd_ref) const
    noexcept(tincup::nothrow_invocable_c<forwarding_ref_cpo_ftor, T>) 
    -> tincup::invocable_t<forwarding_ref_cpo_ftor, T> {
    return tincup::tag_invoke_cpo(*this, std::forward<T>(fwd_ref)...);
  }
} forwarding_ref_cpo;

// Note: operator() methods are provided by cpo_base

// CPO-specific concepts and type aliases for convenient usage
template<typename T>
concept forwarding_ref_cpo_invocable_c = tincup::invocable_c<forwarding_ref_cpo_ftor, T>;

template<typename T>
concept forwarding_ref_cpo_nothrow_invocable_c = tincup::nothrow_invocable_c<forwarding_ref_cpo_ftor, T>;


template<typename T>
using forwarding_ref_cpo_return_t = tincup::invocable_t<forwarding_ref_cpo_ftor, T>;

template<typename T>
using forwarding_ref_cpo_traits = tincup::cpo_traits<forwarding_ref_cpo_ftor, T>;

// Usage: tincup::is_invocable_v<forwarding_ref_cpo_ftor, T>
Variadic Arguments

A CPO that accepts a variable number of arguments

Command:

cpo-generator {"cpo_name": "variadic_cpo", "args": ["$T&...: variadic_args"]}

Generated Code:

inline constexpr struct variadic_cpo_ftor final : tincup::cpo_base<variadic_cpo_ftor> {
  TINCUP_CPO_TAG("variadic_cpo")
  inline static constexpr bool is_variadic = true;
    // Typed operator() overload - positive case only (generic)
  // Negative cases handled by tagged fallback in cpo_base
  template<typename... T>
    requires tincup::invocable_c<variadic_cpo_ftor, T&...>
  constexpr auto operator()(T&... variadic_args) const
    noexcept(tincup::nothrow_invocable_c<variadic_cpo_ftor, T&...>) 
    -> tincup::invocable_t<variadic_cpo_ftor, T&...> {
    return tincup::tag_invoke_cpo(*this, variadic_args...);
  }
} variadic_cpo;

// Note: operator() methods are provided by cpo_base

// CPO-specific concepts and type aliases for convenient usage
template<typename... T>
concept variadic_cpo_invocable_c = tincup::invocable_c<variadic_cpo_ftor, T&...>;

template<typename... T>
concept variadic_cpo_nothrow_invocable_c = tincup::nothrow_invocable_c<variadic_cpo_ftor, T&...>;


template<typename... T>
using variadic_cpo_return_t = tincup::invocable_t<variadic_cpo_ftor, T&...>;

template<typename... T>
using variadic_cpo_traits = tincup::cpo_traits<variadic_cpo_ftor, T&...>;

// Usage: tincup::is_invocable_v<variadic_cpo_ftor, T&...>
Boolean Dispatch CPO

A CPO with runtime boolean dispatch that compiles to zero-overhead compile-time specialization

Command:

cpo-generator {"cpo_name": "conditional_process", "args": ["$T&: data"], "runtime_dispatch": {"type": "bool", "dispatch_arg": "use_fast_path", "options": ["fast", "safe"]}}

Generated Code:

inline constexpr struct conditional_process_ftor final : tincup::cpo_base<conditional_process_ftor> {
  TINCUP_CPO_TAG("conditional_process")
  inline static constexpr bool is_variadic = false;  
  static constexpr struct fast_tag {} fast;
  static constexpr struct safe_tag {} safe;

// Runtime dispatch overload
template<typename T>
constexpr auto operator()(T& data, bool use_fast_path = false) const
  requires (tincup::invocable_c<conditional_process_ftor, T&, fast_tag> && \
            tincup::invocable_c<conditional_process_ftor, T&, safe_tag>)
{
  tincup::BoolDispatch dispatcher(use_fast_path);
  return dispatcher.receive([&](auto dispatch_constant) {
    if constexpr (dispatch_constant.value) {
      return tag_invoke(*this, data, fast);
    } else {
      return tag_invoke(*this, data, safe);
    }
  });
}

// Compile-time dispatch overload for fast
template<typename T>
constexpr auto operator()(T& data, fast_tag) const
  requires (tincup::invocable_c<conditional_process_ftor, T&, fast_tag>)
{
  return tag_invoke(*this, data, fast);
}

// Compile-time dispatch overload for safe
template<typename T>
constexpr auto operator()(T& data, safe_tag) const
  requires (tincup::invocable_c<conditional_process_ftor, T&, safe_tag>)
{
  return tag_invoke(*this, data, safe);
}

} conditional_process;

// Note: operator() methods are provided by cpo_base

// CPO-specific concepts and type aliases for convenient usage
template<typename T>
concept conditional_process_invocable_c = tincup::invocable_c<conditional_process_ftor, T&>;

template<typename T>
concept conditional_process_nothrow_invocable_c = tincup::nothrow_invocable_c<conditional_process_ftor, T&>;


template<typename T>
using conditional_process_return_t = tincup::invocable_t<conditional_process_ftor, T&>;

template<typename T>
using conditional_process_traits = tincup::cpo_traits<conditional_process_ftor, T&>;

// Usage: tincup::is_invocable_v<conditional_process_ftor, T&>

Usage:

// Runtime usage with compile-time optimization
auto result = conditional_process(data, runtime_flag);

// Direct compile-time usage
auto result = conditional_process(data, conditional_process_ftor::fast{});
String Dispatch CPO

A CPO with runtime string dispatch that converts string selection into compile-time specialization

Command:

cpo-generator {"cpo_name": "compression_method", "args": ["$const T&: input"], "runtime_dispatch": {"type": "string", "dispatch_arg": "algorithm", "options": ["lz4", "zstd", "gzip"]}}

Generated Code:

inline constexpr struct compression_method_ftor final : tincup::cpo_base<compression_method_ftor> {
  TINCUP_CPO_TAG("compression_method")
  inline static constexpr bool is_variadic = false;   
  static constexpr struct lz4_tag {} lz4; 
  static constexpr struct zstd_tag {} zstd; 
  static constexpr struct gzip_tag {} gzip;
  static constexpr struct not_found_tag {} not_found;

  // Precomputed options table as a class-scope constant (single instance across TUs)
  inline static constexpr auto options_array = tincup::string_view_array<3>{     "lz4",     "zstd",     "gzip"  };

  // Runtime dispatch overload for string
  template<typename T>
  requires tincup::invocable_c<compression_method_ftor, const T&, compression_method_ftor::not_found_tag>
  constexpr auto operator()(const T& input, std::string_view algorithm) const 
  noexcept(tincup::nothrow_invocable_c<compression_method_ftor, const T&, compression_method_ftor::not_found_tag>) {
    tincup::StringDispatch<3> dispatcher(algorithm, options_array);
    return dispatcher.receive([&](auto dispatch_constant) {
      if constexpr (dispatch_constant.value < 3) {
        if constexpr (dispatch_constant.value == 0) {
          return tag_invoke(*this, input, lz4);
        }
        if constexpr (dispatch_constant.value == 1) {
          return tag_invoke(*this, input, zstd);
        }
        if constexpr (dispatch_constant.value == 2) {
          return tag_invoke(*this, input, gzip);
        }
      } else {
        return tag_invoke(*this, input, not_found);
      }
    });
  }

  // Compile-time dispatch overloads for string
  template<typename T>
  requires tincup::invocable_c<compression_method_ftor, const T&, lz4_tag>
  constexpr auto operator()(const T& input, lz4_tag) const
  noexcept(tincup::nothrow_invocable_c<compression_method_ftor, const T&, lz4_tag>) {
    return tag_invoke(*this, input, lz4);
  }
  template<typename T>
  requires tincup::invocable_c<compression_method_ftor, const T&, zstd_tag>
  constexpr auto operator()(const T& input, zstd_tag) const
  noexcept(tincup::nothrow_invocable_c<compression_method_ftor, const T&, zstd_tag>) {
    return tag_invoke(*this, input, zstd);
  }
  template<typename T>
  requires tincup::invocable_c<compression_method_ftor, const T&, gzip_tag>
  constexpr auto operator()(const T& input, gzip_tag) const
  noexcept(tincup::nothrow_invocable_c<compression_method_ftor, const T&, gzip_tag>) {
    return tag_invoke(*this, input, gzip);
  }
  
  template<typename T>
  requires tincup::invocable_c<compression_method_ftor, const T&, not_found_tag>
  constexpr auto operator()(const T& input, not_found_tag) const
  noexcept(tincup::nothrow_invocable_c<compression_method_ftor, const T&, not_found_tag>) {
    return tag_invoke(*this, input, not_found);
  }

} compression_method;

// Note: operator() methods are provided by cpo_base

// CPO-specific concepts and type aliases for convenient usage
template<typename T>
concept compression_method_invocable_c = tincup::invocable_c<compression_method_ftor, const T&>;

template<typename T>
concept compression_method_nothrow_invocable_c = tincup::nothrow_invocable_c<compression_method_ftor, const T&>;


template<typename T>
using compression_method_return_t = tincup::invocable_t<compression_method_ftor, const T&>;

template<typename T>
using compression_method_traits = tincup::cpo_traits<compression_method_ftor, const T&>;

// Usage: tincup::is_invocable_v<compression_method_ftor, const T&>

Usage:

// Runtime string -> compile-time dispatch (zero overhead after lookup)
auto compressed = compression_method(data, "lz4");

// Direct compile-time usage
auto compressed = compression_method(data, compression_method_ftor::lz4_tag{});

Static Dispatch Benefits:

  • 🚀 Zero runtime overhead after initial dispatch
  • 🎯 Compiler can fully optimize each path independently
  • 🔧 Both runtime convenience and compile-time performance
  • 🛡️ Type-safe dispatch with comprehensive error checking

All-Concrete CPO Design Guidance

TInCuP provides clear guidance when CPOs may not be the right design choice. All-concrete CPOs (those with only concrete types, no generic parameters) are marked with [[deprecated]] to signal they should typically be regular functions instead:

// Generated all-concrete CPO - includes deprecation warning
[[deprecated("CPO with all concrete types - consider using regular function instead. Intended for experimentation/testing only.")]]
inline constexpr struct concrete_example_ftor final : tincup::cpo_base<concrete_example_ftor> {
  TINCUP_CPO_TAG("concrete_example")
  
  // Pure concrete implementation - no concepts needed
  constexpr auto operator()(int& value, const std::string& name) const
    noexcept(noexcept(tincup::tag_invoke_cpo(*this, value, name)))
    -> decltype(tincup::tag_invoke_cpo(*this, value, name)) {
    return tincup::tag_invoke_cpo(*this, value, name);
  }
} concrete_example;

When All-Concrete CPOs Make Sense

Library Consistency: All operations use CPO pattern for uniform API design
Experimentation: Testing CPO patterns before genericizing
Gradual Migration: Converting regular functions to CPOs incrementally
Future Extension: Planning to add generic support later

When Regular Functions Are Better

Single Implementation: Only one implementation will ever exist
No Customization: No third-party customization needed
Simple Logic: Straightforward operations without extension points
Performance Critical: Avoiding any CPO overhead

Technical Implementation

All-concrete CPOs receive special treatment in code generation:

  • No Concept Predicates: Use direct decltype() and noexcept() evaluation instead of concept-based checks
  • Proper ADL: Use fully-qualified tincup::tag_invoke_cpo() for reliable argument-dependent lookup
  • Compile-Time Correctness: Direct type evaluation without template indirection
  • Clear Deprecation Message: Compiler warning explains design implications

Example Compiler Warning:

int value = 42;
std::string name = "test";
auto result = concrete_example(value, name);  // Warning: deprecated CPO usage
warning: 'concrete_example' is deprecated: CPO with all concrete types - 
consider using regular function instead. Intended for experimentation/testing only.

This guidance helps developers make informed architectural decisions while still supporting legitimate use cases for all-concrete CPOs.

Limitations

Code Generator Limitations

  • No Template-Template Parameters: The generator does not support template-template parameters.
  • No Non-Type Template Parameters: The generator does not support non-type template parameters (e.g., template<int N>).

C++20 Language Constraints

TInCuP addresses most issues raised in P2279R0 "We need a language mechanism for customization points" within the bounds of what's possible in C++20. However, some desired features require future language evolution:

Fully Addressed by TInCuP

  • Poor Error Messages: TInCuP's diagnostic system transforms cryptic template errors into educational guidance
  • Boilerplate Complexity: Comprehensive code generation eliminates repetitive tag_invoke patterns
  • Developer Experience: IDE integrations, introspection, and verification tools make CPOs practical

🔶 Partially Addressed

  • Composition: TInCuP provides compose(f, g, ...) utilities, but not seamless language-level composition
  • Discoverability: Auto-generated documentation helps, but requires explicit registration rather than automatic discovery

Requires Future Language Support

  • Syntactic Sugar: P2279R0's desired customizable void f(int x); syntax needs language changes
  • Automatic ADL Isolation: Built-in namespace isolation requires compiler support
  • Seamless Composition: True language-level composition awaits reflection/meta-programming features

Assessment: TInCuP represents the optimal C++20 solution to customization point challenges. While better language mechanisms are eventually needed, TInCuP demonstrates that current pain points can be dramatically reduced through sophisticated tooling and library design.

Third-Party Types

When you need to customize CPOs for types you do not control (for example, Kokkos::View<...>), TInCuP provides a formatter-style extension point that avoids adding symbols to third-party namespaces and requires no wrappers at call sites.

  • Extension point: specialize tincup::cpo_impl<CPO, T> in your project and implement static auto call(T&, Args&&...).
  • Discovery: This trait mirrors the approach used by std::formatter. Specializations live in namespace tincup and do not require modifying third-party headers.
  • Generation support: use the generator to emit a skeleton specialization.

Trait impl targets and generics

  • Use '$Name' in --impl-target to declare a template parameter 'Name'.
  • Use '$Name...' to declare a parameter pack.
  • Use '...' to declare an anonymous pack (maps to 'typename... P').
  • Bare identifiers (e.g., 'double') are treated as concrete types and will not create template parameters.
  • Named packs without '$' (e.g., 'Rest...') are invalid and will error — write '$Rest...' instead.

Examples:

# Single generic parameter
cpo-generator '{"cpo_name":"foo","args":["$V&: x"]}' \
  --impl-target 'MyContainer<$T>' --trait-impl-only

# Head + tail pack (e.g., Kokkos::View first parameter + rest)
cpo-generator '{"cpo_name":"add_in_place","args":["$V&&: y","$const V&: x"]}' \
  --impl-target 'Kokkos::View<$T, $Rest...>' --trait-impl-only

# Concrete first parameter, generic tail pack
cpo-generator '{"cpo_name":"add_in_place","args":["$V&&: y","$const V&: x"]}' \
  --impl-target 'Kokkos::View<double, $Rest...>' --trait-impl-only

The corresponding specialization headers use 'typename' consistently, for example:

namespace tincup {
template<typename T, typename... Rest>
struct cpo_impl<add_in_place_ftor, Kokkos::View<T, Rest...>> {
  template<typename... Args>
  static auto call(Kokkos::View<T, Rest...>& target, Args&&... args) -> /* auto */;
};
} // namespace tincup

Example: generate a trait specialization for a string-dispatch CPO targeting a third-party template type

cpo-generator '{"cpo_name":"execute_policy", "args":["$T&: data"], "runtime_dispatch":{"type":"string","dispatch_arg":"policy","options":["fast","safe","debug"]}}'   --emit-trait-impl --impl-target 'Kokkos::View<...>' --out include/myproj/execute_policy_impl.hpp

The generated skeleton looks like this (simplified):

namespace tincup {
template<class... P>
struct cpo_impl<execute_policy_ftor, Kokkos::View<P...>> {
  template<class... Args>
  static auto call(Kokkos::View<P...>& view, Args&&... args)
      -> /* return type */ {
    // TODO: implement using 'view' and (args...)
  }
};
} // namespace tincup

Notes:

  • You can wrap emission in a macro guard with --impl-guard MACRO.
  • This approach avoids defining functions in third-party namespaces and does not require inheritance or wrappers at call sites.
  • It is suitable for both concrete and templated third-party types; use '...' in --impl-target to denote a template parameter pack (e.g., Kokkos::View<...>).

Trait + ADL Shim (std::vector)

Use an ADL-visible shim in your CPO's namespace that forwards to the tincup::cpo_impl specialization. This keeps the core CPO free of TPL references and requires no wrappers.

  • Command (emit trait + shim):
cpo-generator --from-registry add_in_place_ftor \
  --impl-target 'std::vector<$T, $Alloc>' \
  --emit-trait-impl --emit-adl-shim --shim-namespace 'myproj' \
  --out include/myproj/add_in_place_vector.hpp
  • Result (simplified):
// myproj/add_in_place.hpp (core CPO)
namespace myproj {
inline constexpr struct add_in_place_ftor final : tincup::cpo_base<add_in_place_ftor> {
  TINCUP_CPO_TAG("add_in_place");
} add_in_place;
} // namespace myproj

// Integration header (trait + ADL shim)
namespace tincup {
template<typename T, typename Alloc>
struct cpo_impl<myproj::add_in_place_ftor, std::vector<T, Alloc>> {
  static void call(std::vector<T, Alloc>& y, const std::vector<T, Alloc>& x);
};
} // namespace tincup

namespace myproj {
template<typename T, typename Alloc, typename... Args>
constexpr auto tag_invoke(add_in_place_ftor, std::vector<T, Alloc>& y, Args&&... args)
  noexcept(noexcept(tincup::cpo_impl<add_in_place_ftor, std::vector<T, Alloc>>::call(y, std::forward<Args>(args)...)))
  -> decltype(tincup::cpo_impl<add_in_place_ftor, std::vector<T, Alloc>>::call(y, std::forward<Args>(args)...));
} // namespace myproj
  • Behavior:
    • Calls like myproj::add_in_place(vec, other) resolve to the shim via ADL, which forwards to cpo_impl<...>::call(...).
    • No third-party symbols appear in the core CPO definition.

Vim Plugin

This repository contains a fully functional Vim plugin that integrates the code generator.

Installation

Using a plugin manager like vim-plug, add the following to your .vimrc:

Plug 'sandialabs/TInCuP', {'rtp': 'editor_integration/vim'}

Usage

The plugin provides two commands:

  • :CPO <name> [args...]: Generates a CPO.
  • :CPOD <name> [args...]: Generates a CPO with a Doxygen comment stub.

Example:

:CPO my_cpo $V&:x $const S&:y

Project Structure

tincup/
├── build_systems/             # Integration with build systems
│   ├── cmake/                 # CMake integration
│   ├── make/                  # Development tasks (Makefile)
│   └── meson/                 # Meson integration
├── cpo_tools/                 # Code generation and verification tools
│   └── templates/             # Jinja2 templates for C++ generation
├── docs/                      # Documentation and guides
├── editor_integration/        # Editor/IDE support
│   ├── clion/                 # CLion external tools and templates
│   │   └── file_templates/    # CLion file template definitions
│   ├── vim/                   # Complete Vim plugin
│   │   ├── autoload/          # Vim autoload functions
│   │   ├── cpp_cpo/           # CPO-specific functionality
│   │   └── plugin/            # Vim plugin entry points
│   └── vscode/                # VSCode tasks, snippets, configs
├── examples                   # Demonstration of TInCuP in practice
│   └── serialize
├── include/                   # Header-only library
│   └── tincup/                # Main (only) library header
├── scripts                    # Utilities for updating README examples and copyright banner
└── tests/                     # Comprehensive test suite

For detailed guidance on any integration, see the README in the respective directory.

Editor/IDE Integration

VSCode

See docs/VSCODE_INTEGRATION.md for tasks, snippets, and integration tips.

Vim

The repository includes a complete Vim plugin. See editor_integration/vim/README.md for installation and usage.

CLion

Full CLion integration with external tools, live templates, and file templates. See editor_integration/clion/README.md for setup and usage.

LLM Mode

Use semantic patterns via operation_type to simplify generation. Run cpo-generator --llm-help to list patterns. Current patterns include:

  • mutating_binary: modifies first using second, returns void
  • scalar_mutating: modifies object using a scalar, returns void
  • unary_mutating: modifies object using a unary function, returns void
  • binary_query: computes value from two objects, returns value
  • unary_query: computes value from one object, returns value
  • generator: creates new object from existing, returns new object
  • binary_transform: transforms target using source and a function, returns void

Operation Patterns

Operation Type Description Argument Shape
mutating_binary Modifies first object using second "$T&: target", "$const U&: source"
scalar_mutating Modifies object using a scalar value "$T&: target", "$S: scalar"
unary_mutating Modifies object using a unary function "$T&: target", "$F: func"
binary_query Computes value from two objects "$const T&: lhs", "$const U&: rhs"
unary_query Computes value from one object "$const T&: obj"
generator Creates new object from existing object "$const T&: source"
binary_transform Transforms target using source and a function "$T&: target", "$const U&: source", "$F: func"

Example:

cpo-generator '{"cpo_name": "add_in_place", "operation_type": "mutating_binary"}' --doxygen

Composition

Compose CPOs (or any invocables) left-to-right using tincup::compose:

#include <tincup/tincup.hpp>
using namespace tincup;

// Suppose normalize(x) -> y and stringify(y) -> std::string
inline constexpr struct normalize_ftor final : cpo_base<normalize_ftor> {
  TINCUP_CPO_TAG("normalize")
  template<typename T>
    requires invocable_c<normalize_ftor, T>
    constexpr auto operator()(T&& x) const
    noexcept(nothrow_invocable_c<normalize_ftor, T>) -> invocable_t<normalize_ftor, T> {
      return tag_invoke(*this, std::forward<T>(x));
    }
  template<typename T>
    requires (!invocable_c<normalize_ftor, T>)
    constexpr void operator()(T&& x) const { this->enhanced_fail(std::forward<T>(x)); }
} normalize;

inline constexpr struct stringify_ftor final : cpo_base<stringify_ftor> {
  TINCUP_CPO_TAG("stringify")
  template<typename U>
    requires invocable_c<stringify_ftor, U>
    constexpr auto operator()(U&& u) const
    noexcept(nothrow_invocable_c<stringify_ftor, U>) -> invocable_t<stringify_ftor, U> {
      return tag_invoke(*this, std::forward<U>(u));
    }
  template<typename U>
    requires (!invocable_c<stringify_ftor, U>)
    constexpr void operator()(U&& u) const { this->enhanced_fail(std::forward<U>(u)); }
} stringify;

// Compose: stringify(normalize(x))
auto to_string = compose(normalize, stringify);
// Usage
// std::string s = to_string(obj);

CPO Introspection

TInCuP provides comprehensive introspection capabilities for CPOs through cpo_traits:

#include <tincup/tincup.hpp>
using namespace tincup;

// Example CPO for introspection
struct my_cpo_ftor final : cpo_base<my_cpo_ftor> {
    template<typename T, typename U>
        requires invocable_c<my_cpo_ftor, T&, const U&>
    constexpr auto operator()(T& target, const U& source) const
        noexcept(nothrow_invocable_c<my_cpo_ftor, T&, const U&>)
        -> invocable_t<my_cpo_ftor, T&, const U&> {
        return tag_invoke(*this, target, source);
    }
    
    template<typename T, typename U>
        requires (!invocable_c<my_cpo_ftor, T&, const U&>)
    constexpr void operator()(T& target, const U& source) const {
        this->enhanced_fail(target, source);
    }
} my_cpo;

// Introspect the CPO
using traits = cpo_traits<my_cpo_ftor, std::string&, const int&>;

static_assert(traits::arity == 2);                    // Number of arguments
static_assert(traits::invocable);                     // Can be invoked with these types
static_assert(traits::nothrow_invocable);             // Is noexcept with these types
static_assert(traits::is_void_returning);             // Returns void
static_assert(traits::all_args_are_refs);             // All arguments are references
static_assert(!traits::all_args_are_const_refs);      // Not all are const references

// Access individual argument types
using first_arg = traits::arg_t<0>;   // std::string&
using second_arg = traits::arg_t<1>;  // const int&

// Get a hint about the signature for debugging
constexpr auto sig = traits::signature_hint();  // Returns "(T, U)"

Available Introspection Features

  • invocable: Whether the CPO can be invoked with given argument types
  • nothrow_invocable: Whether the CPO is noexcept with given argument types
  • arity: Number of arguments
  • return_t: Return type (void if not invocable)
  • is_void_returning: Whether the return type is void
  • is_const_invocable: Whether the CPO itself is const-qualified
  • all_args_are_refs: Whether all arguments are reference types
  • all_args_are_const_refs: Whether all arguments are const references
  • arg_t<I>: Type of the I-th argument
  • signature_hint(): String representation for debugging

Supported Compilers

TInCuP is tested on CI with strict C++20 compliance:

  • GCC: 12+ (tested on Ubuntu, comprehensive C++20 support)
  • Clang: 15+ (tested on Ubuntu/macOS, excellent C++20 support)
  • MSVC: 2019 16.11+ / 2022+ (tested on Windows, /std:c++20 /permissive-)

All compilers are tested with:

  • Full C++20 feature set (concepts, consteval, etc.)
  • Template metaprogramming intensive code
  • Strict warning levels (-Wall -Wextra / /W4)
  • Warnings treated as errors for quality assurance

MSVC-Specific Notes

  • Requires /std:c++20 /permissive- flags
  • Tested with both Debug (/Od) and Release (/O2) optimizations
  • Compatible with Visual Studio 2019 16.11+ and Visual Studio 2022
  • Full support for C++20 concepts, consteval, and template features

Vim Installer (Optional)

To help with setup, a helper script prints or appends the correct vimrc snippet for your plugin manager (vim-plug, Vundle, or Pathogen):

# Print recommended lines based on your ~/.vimrc
editor_integration/vim/install.sh

# Append to ~/.vimrc with a timestamped backup
editor_integration/vim/install.sh --apply

# Force a specific manager
editor_integration/vim/install.sh --manager plug   # or vundle | pathogen

Notes:

  • For vim-plug, the script uses {'rtp': 'editor_integration/vim'}.
  • For Vundle/Pathogen, it adds a set rtp+=.../editor_integration/vim line so Vim sees the plugin inside the repo.
  • Without a plugin manager, use native packages: symlink editor_integration/vim/{plugin,autoload} into ~/.vim/pack/tincup/start/tincup/.

Local Testing (Mirrors CI Exactly)

🚀 Test locally before pushing to avoid CI failures:

# Run complete local CI suite (mirrors GitHub Actions exactly)
./scripts/run_local_ci.sh

# Quick development testing (faster subset)
./scripts/run_local_ci.sh --quick

# Individual test categories  
make test-cmake     # Test CMake build
make test-meson     # Test Meson build
make test-python    # Python tests
make test-examples  # Example compilation

Why local testing matters:

  • 2-5 minute feedback vs 10-15 minutes in CI
  • Exact CI parity - same commands, same results
  • Catch issues early - before broken CI commits
  • Debug locally - reproduce CI failures instantly

See docs/LOCAL_TESTING.md for complete guide.

Contributing

🚀 Quick development workflow:

# Development setup
make -f build_systems/make/Makefile install-dev

# Before every commit - runs all checks automatically  
./scripts/checkin.sh   **Use this for automated pre-commit verification**

# Alternative: individual steps
./scripts/run_local_ci.sh  # Complete local CI testing
make -f build_systems/make/Makefile test        # Python tests only
make -f build_systems/make/Makefile verify-cpos # Verify CPO patterns

The checkin.sh script automatically:

  1. 📦 Generates the single header (single_include/tincup.hpp)
  2. 🏷️ Verifies copyright banners on all source files
  3. 🔧 Runs complete local CI (mirrors GitHub Actions exactly)

This ensures your changes are ready for commit and won't break CI.

For detailed contribution guidelines, see CONTRIBUTING.md.

License

This project is licensed under the BSD 3-Clause License. See the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tincup-1.0.4.tar.gz (68.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tincup-1.0.4-py3-none-any.whl (46.3 kB view details)

Uploaded Python 3

File details

Details for the file tincup-1.0.4.tar.gz.

File metadata

  • Download URL: tincup-1.0.4.tar.gz
  • Upload date:
  • Size: 68.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for tincup-1.0.4.tar.gz
Algorithm Hash digest
SHA256 c0b0d3c95728b2576d1b6a29f5587f01945c98eb9002e74312a9ca274e373541
MD5 3b81cff6ba5601daabc2dea780300fe5
BLAKE2b-256 cf8eda8b8be527f631ed5ff41a9b10d33a0296008d38beab19a3fb8649879b15

See more details on using hashes here.

File details

Details for the file tincup-1.0.4-py3-none-any.whl.

File metadata

  • Download URL: tincup-1.0.4-py3-none-any.whl
  • Upload date:
  • Size: 46.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for tincup-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 57aa414b4f4d713a2fabe577367c9747ab07aced2c5a263673a580516bc5a1dd
MD5 63797816cf51ca68d7d7817098d7c419
BLAKE2b-256 514833ad2f798bd41d03e6f6d406f593a81d25d0dae1c057b6fa4159d6381f66

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page