Skip to content

SumOfLogarithms: give the Eigen unary functors a result_type, not a lambda - #58

Merged
lgueguen merged 1 commit into
BioPP:masterfrom
mmokrejs:fix/eigen-unary-functor-needs-result-type
Sep 9, 2026
Merged

lgueguen merged 1 commit into
BioPP:masterfrom
mmokrejs:fix/eigen-unary-functor-needs-result-type

Conversation

@mmokrejs

@mmokrejs mmokrejs commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

SumOfLogarithms passes lambdas to Eigen::unaryExpr. Eigen derives the
resulting expression's Scalar through internal::result_of, and under one
macro combination it takes a pre-C++11 fallback that cannot see a lambda's
return type and defaults to the argument type instead. The expression's
scalar silently becomes double rather than ExtendedFloat, Eigen then tries
to convert the results back through ExtendedFloat::operator double() — which
is private by design — and the build fails deep inside Eigen's headers, naming
neither bpp-phyl nor the real cause.

This replaces the lambdas with small named functors carrying
typedef ExtendedFloat result_type, which is what that fallback looks for. The
deduced scalar is then ExtendedFloat on every path, so the fix does not depend
on which deduction implementation Eigen happens to select.

Behaviour is unchanged — the functors do exactly what the lambdas did. All four
unaryExpr sites in SumOfLogarithms are converted: two that fail today, and
two that carry the same latent defect in an overload that is not currently
instantiated.

Symptom

Eigen/src/Core/Redux.h:204: error:
  'bpp::ExtendedFloat::operator double() const' is private within this context
Eigen/src/Core/CoreEvaluators.h:589: error:
  'bpp::ExtendedFloat::operator double() const' is private within this context

First seen with GCC 16 and Eigen 3.4.0.

Why it is not what it looks like

The conversion operator is private deliberately — "Necessary to keep this
private to prevent misuse"
(ExtendedFloat.h), with
Eigen::internal::nullary_wrapper as its only friend. Eigen reaching it from
Redux.h / CoreEvaluators.h therefore means Eigen had already decided the
expression's scalar is double. The instantiation backtrace says so directly:

Scalar = double; CoeffReturnType = double

So the fix is not to widen the operator's access — that would defeat its
purpose. It is to stop Eigen deducing the wrong scalar.

Root cause

traits<CwiseUnaryOp>::Scalar comes from internal::result_of
(Eigen/src/Core/CwiseUnaryOp.h), which has three implementations selected by
two macros that are mutually exclusive on the wrong side:

macro requires
EIGEN_HAS_STD_RESULT_OF EIGEN_COMP_CXXVER < 17
EIGEN_HAS_STD_INVOKE_RESULT EIGEN_COMP_CXXVER >= 17 and EIGEN_MAX_CPP_VER >= 17

CMakeLists.txt sets CMAKE_CXX_STANDARD 17, so EIGEN_COMP_CXXVER is at
least 17 and EIGEN_HAS_STD_RESULT_OF is 0. If anything caps
EIGEN_MAX_CPP_VER below 17 — a distribution or a toolchain prefix may do this
through CXXFLAGS, without bpp-phyl asking for it — then
EIGEN_HAS_STD_INVOKE_RESULT is 0 as well, and neither modern path is
available.

Eigen falls back to the pre-C++11 unary_result_of_select
(Eigen/src/Core/util/Meta.h), which looks for a result_type member on the
functor and, finding none on a lambda, defaults to the argument type:

template<typename Func, typename ArgType, int SizeOf=sizeof(has_none)>
struct unary_result_of_select { typedef typename remove_all<ArgType>::type type; };

The argument type is double, so the whole expression becomes a double
expression.

Reproducer

Self-contained, against Eigen 3.4.0. The -D forces the macro state described
above, so the failure can be reproduced on any compiler rather than only on the
toolchain that first showed it:

g++ -std=gnu++17 -DEIGEN_MAX_CPP_VER=14 -fsyntax-only \
    -I src -I ../bpp-core/src -I ../bpp-seq/src -I /usr/include/eigen3 \
    src/Bpp/Phyl/Likelihood/DataFlow/DataFlowCWiseComputing.cpp

The macro state can also be inspected directly:

g++ -std=gnu++17 -E -dM -include Eigen/Core -x c++ /dev/null |
  grep -E 'define EIGEN_(MAX_CPP_VER|COMP_CXXVER|HAS_STD_RESULT_OF|HAS_STD_INVOKE_RESULT)'

A healthy configuration shows INVOKE_RESULT 1; the broken one shows
RESULT_OF 0 and INVOKE_RESULT 0 together.

The change

Two small functors, because the sites are not interchangeable — the
single-dependency branch of the plain-Eigen overload calls normalize_small()
while the others call normalize():

  • NormalizeToExtendedFloat
  • NormalizeSmallToExtendedFloat

Converted sites, all in SumOfLogarithms:

  • two in the ExtendedFloatEigen overload — these are the ones that fail today;
  • two in the plain-Eigen overload, which is not instantiated when VectorLik is
    an ExtendedFloatEigen, so it carries the same latent defect without
    reporting it.

Fixing it here rather than by requiring a particular EIGEN_MAX_CPP_VER was
deliberate: the value is set outside the project, and a functor with
result_type is correct under every route into that fallback, not just the one
observed.

Verification

DataFlowCWiseComputing.cpp, Eigen 3.4.0, g++ 14.2:

build before after
-std=gnu++17 0 errors 0 errors
-std=gnu++17 -DEIGEN_MAX_CPP_VER=14 the failure above 0 errors
-std=gnu++17 -DEIGEN_MAX_CPP_VER=11 the failure above 0 errors
-std=gnu++14 0 errors 0 errors
-std=gnu++20 0 errors 0 errors

Separately confirmed on the GCC 16 toolchain where the failure was first
observed: that build previously stopped at this file, and now compiles it and
proceeds through the rest of the library.

-Wall -Weffc++ -Wshadow -Wconversion on that translation unit: 468 → 464
warnings, none added. The remainder is Eigen's own operator&&/operator||
-Weffc++ noise, which merely names the functor type in the instantiation.

Branched from master at 5a57563d, its tip at the time.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

🤖 Generated with Claude Code

Building bpp-phyl can fail inside Eigen with

  Eigen/src/Core/Redux.h:204: error:
    'bpp::ExtendedFloat::operator double() const' is private within this context

naming neither this file nor the real cause. The conversion operator is
private ON PURPOSE ("Necessary to keep this private to prevent misuse",
ExtendedFloat.h), with Eigen::internal::nullary_wrapper as its only friend --
so Eigen reaching it from Redux.h and CoreEvaluators.h means Eigen decided the
expression's Scalar is double and is converting our ExtendedFloat back.

It decided that in internal::result_of, which has three implementations chosen
by two macros that are mutually exclusive on the WRONG side:

  EIGEN_HAS_STD_RESULT_OF      needs EIGEN_COMP_CXXVER <  17
  EIGEN_HAS_STD_INVOKE_RESULT  needs EIGEN_COMP_CXXVER >= 17
                                 AND EIGEN_MAX_CPP_VER >= 17

Compile at C++17 or later while EIGEN_MAX_CPP_VER is capped below 17 -- which a
distribution or a toolchain prefix can do through CXXFLAGS without bpp asking
-- and NEITHER is set. Eigen then falls back to the pre-C++11
unary_result_of_select (Eigen/src/Core/util/Meta.h:544), which looks for a
result_type member and, finding none on a lambda, DEFAULTS TO THE ARGUMENT
TYPE. The CwiseUnaryOp's Scalar becomes double instead of ExtendedFloat.

Measured against Eigen 3.4.0, g++ 14.2, this file:

  -std=gnu++17                          0 errors
  -std=gnu++17 -DEIGEN_MAX_CPP_VER=14   the reported failure, verbatim
  -std=gnu++17 -DEIGEN_MAX_CPP_VER=11   the same

A functor carrying result_type satisfies the legacy path as well as the two
modern ones, so the deduced Scalar is ExtendedFloat under every combination --
including any other route to "both macros unset", which is why this is fixed
here rather than by requiring a particular EIGEN_MAX_CPP_VER.

All four affected sites are converted. Two are in the ExtendedFloatEigen
overload (the ones that fail today); the other two are in the plain-Eigen
overload, which is not instantiated when VectorLik is an ExtendedFloatEigen and
so has the same latent defect without reporting it yet. They need two functors,
not one: the single-dependency branch of the plain-Eigen overload normalizes
SMALL while the others normalize.

After: 0 errors at C++14/17/20, with and without the cap. -Wall -Weffc++
-Wshadow -Wconversion warnings go 468 -> 464 (the remainder are Eigen's own
operator&&/|| -Weffc++ noise, which merely names the functor type).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mmokrejs
mmokrejs marked this pull request as ready for review September 5, 2026 16:36
@lgueguen

lgueguen commented Sep 9, 2026

Copy link
Copy Markdown
Member

Thanks for this precise development
Laurent

@lgueguen
lgueguen merged commit 32a92ba into BioPP:master Sep 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants