Summary
Certain replacement-operator template shapes, when compiled into a C++23 module BMI by clang, "poison" the lookup of that operator/function name for every importer: any TU that imports the module and uses that name on any type crashes the clang frontend (SIGSEGV, exit 139). gcc 16.1.0 is completely unaffected.
This bites the mcpp module-package pattern directly: a module layer that mirrors an upstream header's static inline operator templates (identical signature + trivially-true constraint, the standard mixed-TU subsumption recipe) will hit it as soon as the upstream shape has template parameters that are not all pinned by the first function argument.
- Affected: mcpp llvm toolchains 20.1.7 and 22.1.8 (both crash identically)
- Not affected: gcc 16.1.0; system clang 18.1.3 compiles the same repro fine → regression introduced between clang 18 and 20
- Found in: opencv-m single-repo work (
feat/vendor-single-repo) — the opencv-m gtest suite had never been built with any clang before (macOS/windows CI only build small consumer smokes), so e.g. cv::Point(1,2) * 2 crashed the importer on every clang platform without CI ever noticing.
Standalone repro (40 lines, no OpenCV)
matx.h — mimics an upstream header (TU-local static inline operator templates):
#pragma once
namespace cv {
template<typename T, int m, int n> struct Matx {
T val[m*n];
typedef Matx<T, m, n> mat_type;
enum { rows = m, cols = n };
};
template<typename T> struct Point_ { T x, y; };
using Point = Point_<int>;
template<typename T, int m, int n, int l> static inline
Matx<T, m, n> operator*(const Matx<T, m, l>& a, const Matx<T, l, n>& b) { return Matx<T,m,n>{}; }
template<typename T> static inline
Point_<T> operator*(const Point_<T>& a, int b) { return Point_<T>{a.x*b, a.y*b}; }
}
m.cppm — the module layer (upstream shape + trivially-true constraint, i.e. the subsumption recipe):
module;
#include "matx.h"
export module m;
export namespace cv {
using cv::Matx;
using cv::Point_;
using cv::Point;
inline namespace repl {
template<typename T> inline constexpr bool pick = true;
template<typename T, int m, int n, int l> requires pick<T> inline
Matx<T, m, n> operator*(const Matx<T, m, l>& a, const Matx<T, l, n>& b) { return Matx<T,m,n>{}; }
template<typename T> requires pick<T> inline
Point_<T> operator*(const Point_<T>& a, int b) { return Point_<T>{a.x*b, a.y*b}; }
}
}
use.cpp — note it only multiplies a Point, never a Matx:
import m;
int main() { cv::Point p{1,2}; auto q = p * 2; return q.x; }
$ clang++ -std=c++23 --precompile m.cppm -o m.pcm # fine
$ clang++ -std=c++23 -fprebuilt-module-path=. -c use.cpp
PLEASE submit a bug report ... Stack dump:
#4 clang::Sema::AddArgumentDependentLookupCandidates(...)
#5 clang::Sema::CreateOverloadedBinOp(...)
Control matrix (this exact repro):
| compiler |
with the 4-param Matx*Matx template |
without it |
| clang 20.1.7 (mcpp toolchain) |
SIGSEGV |
ok |
| clang 22.1.8 (mcpp toolchain) |
SIGSEGV |
ok |
| clang 18.1.3 (Ubuntu) |
ok |
ok |
| gcc 16.1.0 |
ok |
ok |
Trigger taxonomy (bisected in opencv-m with a --precompile harness, per-template-block subsets)
Poisonous — template parameters not all pinned by the first argument (or requiring a repeat-consistency check):
template<typename T, int m, int n, int l> op*(Matx<T,m,l>, Matx<T,l,n>) ← the repro above
template<typename T, int m> determinant(const Matx<T,m,m>&) (NTTP repeated inside one parameter type)
template<typename T1, typename T2, int n> op+=(Matx<T1,..>&, const Matx<T2,..>&) (second typename only in arg 2) — same for -=, and for the Vec variants
template<typename T, typename T2, int m, int n> op<<(const Matx<T,m,n>&, T2) (comma-initializer)
NOT poisonous (all verified): same-shape ops where every parameter is pinned by arg 1 — op+(Matx<T,m,n>, Matx<T,m,n>), op*(Matx<T,m,n>, int/float/double), op*(Matx<T,m,n>, Vec<T,n>), (Matx<T,m,n>, Matx<T,n,m>), plain norm/trace(Matx<T,m,n>).
Properties:
- The crash is name-keyed: one poisoned
operator* decl makes every x * y in every importer crash, for unrelated types (Point*2, Mat*2.0, softdouble*softdouble, Size*2 all crashed in opencv-m); operator+ etc. stay fine.
- Function body is irrelevant (trivial-body variant still crashes).
- Two crash flavors, same root: parse-time
AddArgumentDependentLookupCandidates segv, and (via gtest's printer machinery instantiating around such names) unbounded recursive InstantiateFunctionDefinition → stack overflow.
Workaround that ships (opencv-m commit 42aeb20)
Reformulate over whole deduced operand types — call-compatible, and mixed-TU semantics survive because the upstream exact-pattern static-inline is more specialized and wins there:
template<typename MA, typename MB>
requires pick<typename MA::value_type>
&& __is_same(MA, typename MA::mat_type) // "is a Matx"
&& __is_same(MB, Matx<typename MB::value_type, (int)MA::rows, (int)MA::cols>)
inline MA& operator += (MA& a, const MB& b);
With every poisoned shape rewritten this way, opencv-m is green on llvm 20.1.7 and 22.1.8 (6/6 suites), and gcc 16.1.0 regression stays green.
Asks
- Track this as a known clang-toolchain hazard for module packages (docs note in the module-package guidance would save others the bisect: "avoid operator templates whose parameters aren't all pinned by the first argument; deduce whole operand types instead").
- Upstream LLVM report: I can file it with the standalone repro above (clang 18 ok / 20+22 crash gives a clean regression window). Happy to link it back here.
- Possibly related for the toolchain matrix: consider adding a canary to mcpp's llvm-toolchain e2e that imports a module exporting such a shape, so a future clang bump that fixes (or re-breaks) this is visible.
Repro artifacts: standalone 3-file repro (above, self-contained); full-context preprocessed sources + run scripts archived from opencv-m (clang++ -v crash dumps for both 20.1.7 and 22.1.8) — available on request.
Environment: mcpp 0.0.101, linux x86_64 (Ubuntu 24.04, kernel 6.8), toolchains from mcpp toolchain install (llvm 20.1.7 / 22.1.8, gcc 16.1.0).
Summary
Certain replacement-operator template shapes, when compiled into a C++23 module BMI by clang, "poison" the lookup of that operator/function name for every importer: any TU that
imports the module and uses that name on any type crashes the clang frontend (SIGSEGV, exit 139). gcc 16.1.0 is completely unaffected.This bites the mcpp module-package pattern directly: a module layer that mirrors an upstream header's
static inlineoperator templates (identical signature + trivially-true constraint, the standard mixed-TU subsumption recipe) will hit it as soon as the upstream shape has template parameters that are not all pinned by the first function argument.feat/vendor-single-repo) — the opencv-m gtest suite had never been built with any clang before (macOS/windows CI only build small consumer smokes), so e.g.cv::Point(1,2) * 2crashed the importer on every clang platform without CI ever noticing.Standalone repro (40 lines, no OpenCV)
matx.h— mimics an upstream header (TU-localstatic inlineoperator templates):m.cppm— the module layer (upstream shape + trivially-true constraint, i.e. the subsumption recipe):use.cpp— note it only multiplies a Point, never a Matx:Control matrix (this exact repro):
Matx*MatxtemplateTrigger taxonomy (bisected in opencv-m with a
--precompileharness, per-template-block subsets)Poisonous — template parameters not all pinned by the first argument (or requiring a repeat-consistency check):
template<typename T, int m, int n, int l> op*(Matx<T,m,l>, Matx<T,l,n>)← the repro abovetemplate<typename T, int m> determinant(const Matx<T,m,m>&)(NTTP repeated inside one parameter type)template<typename T1, typename T2, int n> op+=(Matx<T1,..>&, const Matx<T2,..>&)(second typename only in arg 2) — same for-=, and for theVecvariantstemplate<typename T, typename T2, int m, int n> op<<(const Matx<T,m,n>&, T2)(comma-initializer)NOT poisonous (all verified): same-shape ops where every parameter is pinned by arg 1 —
op+(Matx<T,m,n>, Matx<T,m,n>),op*(Matx<T,m,n>, int/float/double),op*(Matx<T,m,n>, Vec<T,n>),(Matx<T,m,n>, Matx<T,n,m>), plainnorm/trace(Matx<T,m,n>).Properties:
operator*decl makes everyx * yin every importer crash, for unrelated types (Point*2,Mat*2.0,softdouble*softdouble,Size*2all crashed in opencv-m);operator+etc. stay fine.AddArgumentDependentLookupCandidatessegv, and (via gtest's printer machinery instantiating around such names) unbounded recursiveInstantiateFunctionDefinition→ stack overflow.Workaround that ships (opencv-m commit 42aeb20)
Reformulate over whole deduced operand types — call-compatible, and mixed-TU semantics survive because the upstream exact-pattern static-inline is more specialized and wins there:
With every poisoned shape rewritten this way, opencv-m is green on llvm 20.1.7 and 22.1.8 (6/6 suites), and gcc 16.1.0 regression stays green.
Asks
Repro artifacts: standalone 3-file repro (above, self-contained); full-context preprocessed sources + run scripts archived from opencv-m (
clang++ -vcrash dumps for both 20.1.7 and 22.1.8) — available on request.Environment: mcpp 0.0.101, linux x86_64 (Ubuntu 24.04, kernel 6.8), toolchains from
mcpp toolchain install(llvm 20.1.7 / 22.1.8, gcc 16.1.0).