https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127218

            Bug ID: 127218
           Summary: [13/14/15/16/17 Regression] constexpr
                    memchr/strchr/strrchr/strstr count a nonzero offset in
                    the first argument twice
           Product: gcc
           Version: 16.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: c++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: stevensudit at gmail dot com
  Target Milestone: ---

Created attachment 65501
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65501&action=edit
Reduced testcase (proposed g++.dg/cpp0x/constexpr-memchr2.C)

The C++ front end's constant evaluator returns the wrong pointer from
__builtin_memchr, __builtin_strchr, __builtin_strrchr and __builtin_strstr
whenever the first argument is a constant array plus a nonzero offset: the
offset is added twice.

  constexpr const char a[] = "abcdefghijabxdefghijaaa";
  static_assert (__builtin_memchr (a, 'x', sizeof (a) - 1) == a + 12, "");   
// OK
  static_assert (__builtin_memchr (a + 1, 'a', sizeof (a) - 2) == a + 10, "");
// fails: a + 11
  static_assert (__builtin_memchr (a + 2, 'a', sizeof (a) - 3) == a + 10, "");
// fails: a + 12
  static_assert (__builtin_memchr (&a[1], 'a', sizeof (a) - 2) == a + 10, "");
// OK (ADDR_EXPR)
  static_assert (__builtin_strchr (a + 1, 'a') == a + 10, "");                
// fails
  static_assert (__builtin_strrchr (a + 1, 'b') == a + 11, "");               
// fails
  static_assert (__builtin_strstr (a + 1, "ab") == a + 10, "");               
// fails
  static_assert (__builtin_strlen (a + 1) == 22, "");                         
// OK

Fails identically with -std=c++11 through -std=c++26; the "+0" forms, the
&a[1] form, the nullptr results and the length-returning builtins are
correct. The attached constexpr-memchr2.C has 18 static_asserts of which
the 13 with an offset first argument fail:

  $ g++ -std=c++11 -fsyntax-only constexpr-memchr2.C
  constexpr-memchr2.C:8:62: error: static assertion failed
  constexpr-memchr2.C:9:62: error: static assertion failed
  constexpr-memchr2.C:10:62: error: static assertion failed
  constexpr-memchr2.C:13:46: error: static assertion failed
  constexpr-memchr2.C:14:47: error: static assertion failed
  constexpr-memchr2.C:15:47: error: static assertion failed
  constexpr-memchr2.C:16:47: error: static assertion failed
  constexpr-memchr2.C:17:47: error: static assertion failed
  constexpr-memchr2.C:18:45: error: static assertion failed
  constexpr-memchr2.C:27:48: error: static assertion failed
  constexpr-memchr2.C:28:32: error: static assertion failed
  constexpr-memchr2.C:29:32: error: static assertion failed
  constexpr-memchr2.C:30:33: error: static assertion failed

Compiler Explorer x86-64 builds: gcc 9.5 accepts the whole file; gcc 10.1,
13.4, 14.4, 15.2, 16.2 and trunk (2026-09-04) reject those 13 lines.
Locally reproduced with Ubuntu gcc 13.3.0 and 15.2.0 (same 13 lines).
clang 23 accepts the memchr, strchr and strlen lines (it does not
constant-evaluate strrchr or strstr at all, so those are not a cross-check).

  Compiler Explorer gcc 16.2:
  Target: x86_64-linux-gnu
  Configured with: ../gcc-16.2.0/configure
--prefix=/opt/compiler-explorer/gcc-build/staging --build=x86_64-linux-gnu
--host=x86_64-linux-gnu --target=x86_64-linux-gnu --disable-bootstrap
--enable-multiarch --with-abi=m64 --with-multilib-list=m32,m64,mx32
--enable-multilib --enable-clocale=gnu
--enable-languages=c,c++,fortran,ada,objc,obj-c++,go,d,m2,rust,cobol,algol68
--enable-ld=yes --enable-gold=yes --enable-libstdcxx-time=yes
--enable-linker-build-id --enable-lto --enable-plugins --enable-threads=posix
--with-pkgversion=Compiler-Explorer-Build-gcc--binutils-2.44
  Thread model: posix
  gcc version 16.2.0 (Compiler-Explorer-Build-gcc--binutils-2.44)

This became user-visible in C++26 mode with libstdc++: P2738 makes void* ->
T* casts constant expressions, so the front end now folds
char_traits<char>::find on a constant haystack instead of leaving memchr to
the middle end, and std::string_view::find (c, pos) returns pos too much:

  #include <cstdio>
  #include <string_view>
  int main ()
  {
    constexpr std::string_view s = "abcdefghijabxdefghijaaa";
    static constexpr const char a[] = "abcdefghijabxdefghijaaa";
    const char *p = static_cast<const char *> (__builtin_memchr (a + 1, 'a',
sizeof (a) - 2));
    std::printf ("__builtin_memchr (a + 1, 'a', n) - a = %td (expected 10)\n",
p - a);
    std::printf ("s.find ('a', 1) = %zu (expected 10)\n", s.find ('a', 1));
  }

  g++ 16.2 -std=c++26 -O0:  11 / 10      (the cast is folded by the front end)
  g++ 16.2 -std=c++26 -O2:  11 / 11      (cp_fold folds the inlined find too)
  g++ 16.2 -std=c++23 -O2:  10 / 10
  g++ 9.5  -std=c++17 -O2:  10 / 10
  (Compiler Explorer, 2026-09-04; trunk and Ubuntu 15.2.0 behave like 16.2.)

Cause

Introduced by r10-5927-g69dc042f91c70 (PR c++/80265, "constexpr
__builtin_mem*"). cxx_eval_builtin_function_call replaces string arguments
of the str/mem builtins with the address of a STRING_CST before folding, and
for the pointer-returning builtins (strret) afterwards puts the original
first argument back as the base of the folded POINTER_PLUS_EXPR
(gcc/cp/constexpr.cc:3055 on current trunk):

  if (strret)
    {
      /* memchr returns a pointer into the first argument, but we replaced the
         argument above with a STRING_CST; put it back it now.  */
      tree op = CALL_EXPR_ARG (t, strret-1);
      STRIP_NOPS (new_call);
      if (TREE_CODE (new_call) == POINTER_PLUS_EXPR)
        TREE_OPERAND (new_call, 0) = op;
      else if (TREE_CODE (new_call) == ADDR_EXPR)
        new_call = op;
    }

The replacement only happens when the evaluated argument is a bare ADDR_EXPR
(offset 0); an argument such as a + 1 is passed to fold unchanged,
fold_builtin_call_array merges the offset into the result (a + 10), and the
unconditional re-basing then turns that into (a + 1) + 10. &a[1] is
unaffected because the evaluated argument is an ADDR_EXPR, so the
replacement path is taken and the re-basing is correct.

The existing tests (constexpr-memchr.C, constexpr-strchr.C,
constexpr-strstr.C) never pass an offset first argument, which is why this
was not caught.

Fix

Remember which argument was replaced (strret_op) and re-base only in that
case. Patch with a testsuite addition attached; it applies to trunk
(r17-3939-g08794c636095, 2026-09-04) and to 16.2.0. The new test passes with
-std=c++11/14/17/20/23/26 on a patched 16.2.0 and fails on the 13 lines
above on stock 16.2.0. A full bootstrap and regression test of the patch
have not been run.

Reply via email to