On Tue Dec 16, 2025 at 1:28 PM CET, Peter Eisentraut wrote:
I think it might be good to create a test extension written in C++, like under src/test/modules/, and sprinkle it with various constructs like copyObject() and static assertions, and whatever else we find that is possibly problematic.

Attached is a patchset that does that. It required a few more fixes to
make the extension compile on MSVC too.
From 290a1ab7788756adaed14dc2b483156d65bc3b95 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Fri, 5 Dec 2025 15:37:59 +0100
Subject: [PATCH v4 1/4] Make copyObject work in C++

Calling copyObject in C++ without GNU extensions fails with an error
like this:

error: use of undeclared identifier 'typeof'; did you mean 'typeid'

This is due to the C compiler supporting used to compile postgres
supporting typeof, but that function actually not being present in the
C++ compiler. This fixes that by defining pg_exprtype which maps to
typeof or decltype depending on the compiler. While pg_typeof would have
been a more natural name, that name is already taken in our codebase as
the implementation of the pg_typeof UDF.
---
 src/include/c.h           | 13 +++++++++++++
 src/include/nodes/nodes.h |  4 ++--
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/src/include/c.h b/src/include/c.h
index 77c754a904a..18bd50da510 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -418,6 +418,19 @@
 #define unlikely(x) ((x) != 0)
 #endif
 
+/*
+ * pg_exprtype
+ *		Get the type of an expression at compile time.
+ *
+ * In C++ we use decltype since typeof is not standard C++, while in C we use
+ * typeof when available.
+ */
+#if defined(__cplusplus)
+#define pg_exprtype(x) decltype(x)
+#elif defined(HAVE_TYPEOF)
+#define pg_exprtype(x) typeof(x)
+#endif
+
 /*
  * CppAsString
  *		Convert the argument to a string, using the C preprocessor.
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index b6ad28618ab..f5e17e670b7 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -226,8 +226,8 @@ extern int16 *readAttrNumberCols(int numCols);
 extern void *copyObjectImpl(const void *from);
 
 /* cast result back to argument type, if supported by compiler */
-#ifdef HAVE_TYPEOF
-#define copyObject(obj) ((typeof(obj)) copyObjectImpl(obj))
+#ifdef pg_exprtype
+#define copyObject(obj) ((pg_exprtype(obj)) copyObjectImpl(obj))
 #else
 #define copyObject(obj) copyObjectImpl(obj)
 #endif

base-commit: 094b61ce3ebbb1258675cb9b4eca9198628e2177
-- 
2.52.0

From f9693ae786b0859c627098ec9a47f048e55a1fa5 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Mon, 8 Dec 2025 08:13:51 +0100
Subject: [PATCH v4 2/4] Use pg_exprtype instead of typeof

The previous commit introduced pg_exprtype. This starts using that in a
few more places.
---
 src/include/c.h            | 8 ++++----
 src/include/utils/relptr.h | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/include/c.h b/src/include/c.h
index 18bd50da510..beed3a1a52d 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -976,10 +976,10 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName,
  */
 #ifdef HAVE__BUILTIN_TYPES_COMPATIBLE_P
 #define AssertVariableIsOfType(varname, typename) \
-	StaticAssertStmt(__builtin_types_compatible_p(__typeof__(varname), typename), \
+	StaticAssertStmt(__builtin_types_compatible_p(pg_exprtype(varname), typename), \
 	CppAsString(varname) " does not have type " CppAsString(typename))
 #define AssertVariableIsOfTypeMacro(varname, typename) \
-	(StaticAssertExpr(__builtin_types_compatible_p(__typeof__(varname), typename), \
+	(StaticAssertExpr(__builtin_types_compatible_p(pg_exprtype(varname), typename), \
 	 CppAsString(varname) " does not have type " CppAsString(typename)))
 #else							/* !HAVE__BUILTIN_TYPES_COMPATIBLE_P */
 #define AssertVariableIsOfType(varname, typename) \
@@ -1227,11 +1227,11 @@ typedef struct PGAlignedXLogBlock
 #define unvolatize(underlying_type, expr) const_cast<underlying_type>(expr)
 #elif defined(HAVE__BUILTIN_TYPES_COMPATIBLE_P)
 #define unconstify(underlying_type, expr) \
-	(StaticAssertExpr(__builtin_types_compatible_p(__typeof(expr), const underlying_type), \
+	(StaticAssertExpr(__builtin_types_compatible_p(pg_exprtype(expr), const underlying_type), \
 					  "wrong cast"), \
 	 (underlying_type) (expr))
 #define unvolatize(underlying_type, expr) \
-	(StaticAssertExpr(__builtin_types_compatible_p(__typeof(expr), volatile underlying_type), \
+	(StaticAssertExpr(__builtin_types_compatible_p(pg_exprtype(expr), volatile underlying_type), \
 					  "wrong cast"), \
 	 (underlying_type) (expr))
 #else
diff --git a/src/include/utils/relptr.h b/src/include/utils/relptr.h
index aeb17fa24a5..3e03d34d9ad 100644
--- a/src/include/utils/relptr.h
+++ b/src/include/utils/relptr.h
@@ -38,10 +38,10 @@
 #define relptr_declare(type, relptrtype) \
 	typedef relptr(type) relptrtype
 
-#ifdef HAVE_TYPEOF
+#ifdef pg_exprtype
 #define relptr_access(base, rp) \
 	(AssertVariableIsOfTypeMacro(base, char *), \
-	 (typeof((rp).relptr_type)) ((rp).relptr_off == 0 ? NULL : \
+	 (pg_exprtype((rp).relptr_type)) ((rp).relptr_off == 0 ? NULL : \
 		(base) + (rp).relptr_off - 1))
 #else
 #define relptr_access(base, rp) \
@@ -68,10 +68,10 @@ relptr_store_eval(char *base, char *val)
 	}
 }
 
-#ifdef HAVE_TYPEOF
+#ifdef pg_exprtype
 #define relptr_store(base, rp, val) \
 	(AssertVariableIsOfTypeMacro(base, char *), \
-	 AssertVariableIsOfTypeMacro(val, typeof((rp).relptr_type)), \
+	 AssertVariableIsOfTypeMacro(val, pg_exprtype((rp).relptr_type)), \
 	 (rp).relptr_off = relptr_store_eval((base), (char *) (val)))
 #else
 #define relptr_store(base, rp, val) \
-- 
2.52.0

From cbdc5128717aa0f2c1c0ec3592bdfb0ff09863a2 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Sat, 3 Jan 2026 00:43:59 +0100
Subject: [PATCH v4 3/4] Revert "Replace pg_restrict by standard restrict"

This reverts commit f0f2c0c1aef95757c4e7f144d5577e2b0d814279.

It turns out that this completely broke compiling C++ extensions on
MSVC. The restrict replacement with __restrict would break usages of
__decltype(restrict) in standard library headers, resulting in errors
like this:

C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\ucrt\corecrt_malloc.h(58): error C2485: '__restrict': unrecognized extended attribute
---
 configure                                 | 18 ++++++++++++++----
 configure.ac                              | 16 ++++++++++++----
 meson.build                               | 13 +++++++------
 src/bin/pg_verifybackup/pg_verifybackup.c |  4 ++--
 src/bin/pg_verifybackup/pg_verifybackup.h |  4 ++--
 src/common/logging.c                      |  4 ++--
 src/common/string.c                       |  2 +-
 src/include/c.h                           |  6 ------
 src/include/common/logging.h              |  4 ++--
 src/include/common/string.h               |  2 +-
 src/include/libpq/pqformat.h              | 12 ++++++------
 src/include/pg_config.h.in                |  4 ++++
 12 files changed, 53 insertions(+), 36 deletions(-)

diff --git a/configure b/configure
index 78597c6229a..26883131d4c 100755
--- a/configure
+++ b/configure
@@ -15131,10 +15131,10 @@ _ACEOF
 fi
 
 
-# Even though restrict is in C99 and should be supported by all
-# supported compilers, this test is useful because it will prefer a
-# spelling that also works in C++ (often __restrict).  (restrict is
-# not part of the C++ standard.)
+# MSVC doesn't cope well with defining restrict to __restrict, the
+# spelling it understands, because it conflicts with
+# __declspec(restrict). Therefore we define pg_restrict to the
+# appropriate definition, which presumably won't conflict.
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for C/C++ restrict keyword" >&5
 $as_echo_n "checking for C/C++ restrict keyword... " >&6; }
 if ${ac_cv_c_restrict+:} false; then :
@@ -15181,6 +15181,16 @@ _ACEOF
  ;;
  esac
 
+if test "$ac_cv_c_restrict" = "no"; then
+  pg_restrict=""
+else
+  pg_restrict="$ac_cv_c_restrict"
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define pg_restrict $pg_restrict
+_ACEOF
+
 
 ac_fn_c_check_type "$LINENO" "struct option" "ac_cv_type_struct_option" "#ifdef HAVE_GETOPT_H
 #include <getopt.h>
diff --git a/configure.ac b/configure.ac
index 2ccf410f94c..08da448c1f3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1689,11 +1689,19 @@ PGAC_UNION_SEMUN
 AC_CHECK_TYPES(socklen_t, [], [], [#include <sys/socket.h>])
 PGAC_STRUCT_SOCKADDR_SA_LEN
 
-# Even though restrict is in C99 and should be supported by all
-# supported compilers, this test is useful because it will prefer a
-# spelling that also works in C++ (often __restrict).  (restrict is
-# not part of the C++ standard.)
+# MSVC doesn't cope well with defining restrict to __restrict, the
+# spelling it understands, because it conflicts with
+# __declspec(restrict). Therefore we define pg_restrict to the
+# appropriate definition, which presumably won't conflict.
 AC_C_RESTRICT
+if test "$ac_cv_c_restrict" = "no"; then
+  pg_restrict=""
+else
+  pg_restrict="$ac_cv_c_restrict"
+fi
+AC_DEFINE_UNQUOTED([pg_restrict], [$pg_restrict],
+[Define to keyword to use for C99 restrict support, or to nothing if not
+supported])
 
 AC_CHECK_TYPES([struct option], [], [],
 [#ifdef HAVE_GETOPT_H
diff --git a/meson.build b/meson.build
index 467f7f005a6..cc3ded782e4 100644
--- a/meson.build
+++ b/meson.build
@@ -2825,12 +2825,13 @@ int main(void)
 endforeach
 
 
-# Even though restrict is in C99 and should be supported by all
-# supported compilers, this indirection is useful because __restrict
-# also works in C++ in all supported compilers.  (If not, then we
-# might have to write a real test.)  (restrict is not part of the C++
-# standard.)
-cdata.set('restrict', '__restrict')
+# MSVC doesn't cope well with defining restrict to __restrict, the spelling it
+# understands, because it conflicts with __declspec(restrict). Therefore we
+# define pg_restrict to the appropriate definition, which presumably won't
+# conflict.
+#
+# We assume C99 support, so we don't need to make this conditional.
+cdata.set('pg_restrict', '__restrict')
 
 
 # Most libraries are included only if they demonstrably provide a function we
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 456e0375e13..f9f2d457f2f 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -1228,7 +1228,7 @@ parse_required_wal(verifier_context *context, char *pg_waldump_path,
  * context says we should.
  */
 void
-report_backup_error(verifier_context *context, const char *restrict fmt,...)
+report_backup_error(verifier_context *context, const char *pg_restrict fmt,...)
 {
 	va_list		ap;
 
@@ -1245,7 +1245,7 @@ report_backup_error(verifier_context *context, const char *restrict fmt,...)
  * Report a fatal error and exit
  */
 void
-report_fatal_error(const char *restrict fmt,...)
+report_fatal_error(const char *pg_restrict fmt,...)
 {
 	va_list		ap;
 
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.h b/src/bin/pg_verifybackup/pg_verifybackup.h
index 1cd76a8826b..a99d9bfd581 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.h
+++ b/src/bin/pg_verifybackup/pg_verifybackup.h
@@ -96,9 +96,9 @@ typedef struct verifier_context
 } verifier_context;
 
 extern void report_backup_error(verifier_context *context,
-								const char *restrict fmt,...)
+								const char *pg_restrict fmt,...)
 			pg_attribute_printf(2, 3);
-pg_noreturn extern void report_fatal_error(const char *restrict fmt,...)
+pg_noreturn extern void report_fatal_error(const char *pg_restrict fmt,...)
 			pg_attribute_printf(1, 2);
 extern bool should_ignore_relpath(verifier_context *context,
 								  const char *relpath);
diff --git a/src/common/logging.c b/src/common/logging.c
index 214a87f2189..5206949e5d8 100644
--- a/src/common/logging.c
+++ b/src/common/logging.c
@@ -206,7 +206,7 @@ pg_logging_set_locus_callback(void (*cb) (const char **filename, uint64 *lineno)
 
 void
 pg_log_generic(enum pg_log_level level, enum pg_log_part part,
-			   const char *restrict fmt,...)
+			   const char *pg_restrict fmt,...)
 {
 	va_list		ap;
 
@@ -217,7 +217,7 @@ pg_log_generic(enum pg_log_level level, enum pg_log_part part,
 
 void
 pg_log_generic_v(enum pg_log_level level, enum pg_log_part part,
-				 const char *restrict fmt, va_list ap)
+				 const char *pg_restrict fmt, va_list ap)
 {
 	int			save_errno = errno;
 	const char *filename = NULL;
diff --git a/src/common/string.c b/src/common/string.c
index fff14d3facf..41c74a1502d 100644
--- a/src/common/string.c
+++ b/src/common/string.c
@@ -47,7 +47,7 @@ pg_str_endswith(const char *str, const char *end)
  * strtoint --- just like strtol, but returns int not long
  */
 int
-strtoint(const char *restrict str, char **restrict endptr, int base)
+strtoint(const char *pg_restrict str, char **pg_restrict endptr, int base)
 {
 	long		val;
 
diff --git a/src/include/c.h b/src/include/c.h
index beed3a1a52d..df6c753d908 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -108,12 +108,6 @@
 #define inline
 #endif
 
-/*
- * Previously used PostgreSQL-specific spelling, for backward compatibility
- * for extensions.
- */
-#define pg_restrict restrict
-
 /*
  * Attribute macros
  *
diff --git a/src/include/common/logging.h b/src/include/common/logging.h
index b5caec0bff1..bccba4ac07b 100644
--- a/src/include/common/logging.h
+++ b/src/include/common/logging.h
@@ -93,10 +93,10 @@ void		pg_logging_set_pre_callback(void (*cb) (void));
 void		pg_logging_set_locus_callback(void (*cb) (const char **filename, uint64 *lineno));
 
 void		pg_log_generic(enum pg_log_level level, enum pg_log_part part,
-						   const char *restrict fmt,...)
+						   const char *pg_restrict fmt,...)
 			pg_attribute_printf(3, 4);
 void		pg_log_generic_v(enum pg_log_level level, enum pg_log_part part,
-							 const char *restrict fmt, va_list ap)
+							 const char *pg_restrict fmt, va_list ap)
 			pg_attribute_printf(3, 0);
 
 /*
diff --git a/src/include/common/string.h b/src/include/common/string.h
index 5e1bd61c710..2a7c31ea74e 100644
--- a/src/include/common/string.h
+++ b/src/include/common/string.h
@@ -25,7 +25,7 @@ typedef struct PromptInterruptContext
 
 /* functions in src/common/string.c */
 extern bool pg_str_endswith(const char *str, const char *end);
-extern int	strtoint(const char *restrict str, char **restrict endptr,
+extern int	strtoint(const char *pg_restrict str, char **pg_restrict endptr,
 					 int base);
 extern char *pg_clean_ascii(const char *str, int alloc_flags);
 extern int	pg_strip_crlf(char *str);
diff --git a/src/include/libpq/pqformat.h b/src/include/libpq/pqformat.h
index 911d9397f5e..bc4ab1381a9 100644
--- a/src/include/libpq/pqformat.h
+++ b/src/include/libpq/pqformat.h
@@ -34,7 +34,7 @@ extern void pq_sendfloat8(StringInfo buf, float8 f);
  * Append a [u]int8 to a StringInfo buffer, which already has enough space
  * preallocated.
  *
- * The use of restrict allows the compiler to optimize the code based on
+ * The use of pg_restrict allows the compiler to optimize the code based on
  * the assumption that buf, buf->len, buf->data and *buf->data don't
  * overlap. Without the annotation buf->len etc cannot be kept in a register
  * over subsequent pq_writeintN calls.
@@ -43,7 +43,7 @@ extern void pq_sendfloat8(StringInfo buf, float8 f);
  * overly picky and demanding a * before a restrict.
  */
 static inline void
-pq_writeint8(StringInfoData *restrict buf, uint8 i)
+pq_writeint8(StringInfoData *pg_restrict buf, uint8 i)
 {
 	uint8		ni = i;
 
@@ -57,7 +57,7 @@ pq_writeint8(StringInfoData *restrict buf, uint8 i)
  * preallocated.
  */
 static inline void
-pq_writeint16(StringInfoData *restrict buf, uint16 i)
+pq_writeint16(StringInfoData *pg_restrict buf, uint16 i)
 {
 	uint16		ni = pg_hton16(i);
 
@@ -71,7 +71,7 @@ pq_writeint16(StringInfoData *restrict buf, uint16 i)
  * preallocated.
  */
 static inline void
-pq_writeint32(StringInfoData *restrict buf, uint32 i)
+pq_writeint32(StringInfoData *pg_restrict buf, uint32 i)
 {
 	uint32		ni = pg_hton32(i);
 
@@ -85,7 +85,7 @@ pq_writeint32(StringInfoData *restrict buf, uint32 i)
  * preallocated.
  */
 static inline void
-pq_writeint64(StringInfoData *restrict buf, uint64 i)
+pq_writeint64(StringInfoData *pg_restrict buf, uint64 i)
 {
 	uint64		ni = pg_hton64(i);
 
@@ -105,7 +105,7 @@ pq_writeint64(StringInfoData *restrict buf, uint64 i)
  * sent to the frontend.
  */
 static inline void
-pq_writestring(StringInfoData *restrict buf, const char *restrict str)
+pq_writestring(StringInfoData *pg_restrict buf, const char *pg_restrict str)
 {
 	int			slen = strlen(str);
 	char	   *p;
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index f0091b09cbe..bc68a815e15 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -799,6 +799,10 @@
 #undef inline
 #endif
 
+/* Define to keyword to use for C99 restrict support, or to nothing if not
+   supported */
+#undef pg_restrict
+
 /* Define to the equivalent of the C99 'restrict' keyword, or to
    nothing if this is not supported.  Do not define if restrict is
    supported directly.  */
-- 
2.52.0

From d58a560966d53640af8f7f24882ba004221f35ef Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Fri, 2 Jan 2026 22:31:05 +0100
Subject: [PATCH v4 4/4] tests: Add a test C++ extension module

While we already test that our headers are valid C++ using headerscheck,
it turns out that the macros we define might still expand to invalid
C++ code. This adds a small test extension that is compiled using C++
which uses a few macros that have caused problems in the passed or we
expect might cause problems in the future. This is definitely not meant
to be exhaustive. It's main purpose is to serve as a regression test for
previous failures, and as a place where future failures can easily be
added.

To get CI green, this also fixes a few issues when compiling C++
extensions on MSVC. Notably, our use of designated initializers in
common macros means that on MSVC we essentially need C++20. Given that
no-one has complained about that yet, it seems unlikely that anyone is
currently compiling C++ extensions on MSVC with a lower standard. GCC
and clang allow such initializers in earlier standards in also, even
though they only became an official part of C++20.
---
 .cirrus.tasks.yml                             |  6 ++-
 meson.build                                   |  4 ++
 src/include/c.h                               | 18 ++++++-
 src/include/nodes/pg_list.h                   |  8 +--
 src/test/modules/Makefile                     |  1 +
 src/test/modules/meson.build                  |  1 +
 src/test/modules/test_cplusplusext/Makefile   | 24 +++++++++
 .../modules/test_cplusplusext/meson.build     | 35 +++++++++++++
 .../test_cplusplusext/test_cplusplusext.cpp   | 51 +++++++++++++++++++
 9 files changed, 140 insertions(+), 8 deletions(-)
 create mode 100644 src/test/modules/test_cplusplusext/Makefile
 create mode 100644 src/test/modules/test_cplusplusext/meson.build
 create mode 100644 src/test/modules/test_cplusplusext/test_cplusplusext.cpp

diff --git a/.cirrus.tasks.yml b/.cirrus.tasks.yml
index 038d043d00e..94abf63265c 100644
--- a/.cirrus.tasks.yml
+++ b/.cirrus.tasks.yml
@@ -453,7 +453,8 @@ task:
 
     # SANITIZER_FLAGS is set in the tasks below
     CFLAGS: -Og -ggdb -fno-sanitize-recover=all $SANITIZER_FLAGS
-    CXXFLAGS: $CFLAGS
+    # Use -std=c++11 (not gnu++11) to catch C++ portability issues
+    CXXFLAGS: $CFLAGS -std=c++11
     LDFLAGS: $SANITIZER_FLAGS
     CC: ccache gcc
     CXX: ccache g++
@@ -573,6 +574,7 @@ task:
         su postgres <<-EOF
           set -e
           export CC='ccache gcc -m32'
+          export CXX='ccache g++ -m32'
           meson setup \
             ${MESON_COMMON_PG_CONFIG_ARGS} \
             --buildtype=debug \
@@ -806,7 +808,7 @@ task:
 
   configure_script: |
     vcvarsall x64
-    meson setup --backend ninja %MESON_COMMON_PG_CONFIG_ARGS% --buildtype debug -Db_pch=true -Dextra_lib_dirs=c:\openssl\1.1\lib -Dextra_include_dirs=c:\openssl\1.1\include -DTAR=%TAR% %MESON_FEATURES% build
+    meson setup --backend ninja %MESON_COMMON_PG_CONFIG_ARGS% --buildtype debug -Db_pch=true -Dextra_lib_dirs=c:\openssl\1.1\lib -Dextra_include_dirs=c:\openssl\1.1\include -DTAR=%TAR% -Dcpp_args=/std:c++20 %MESON_FEATURES% build
 
   build_script: |
     vcvarsall x64
diff --git a/meson.build b/meson.build
index cc3ded782e4..4a63904870e 100644
--- a/meson.build
+++ b/meson.build
@@ -2185,6 +2185,10 @@ if cc.get_id() == 'msvc'
     '/w24777', # 'function' : format string 'string' requires an argument of type 'type1', but variadic argument number has type 'type2' [like -Wformat]
   ]
 
+  cxxflags_warn += [
+    '/wd4200', # nonstandard extension used: zero-sized array in struct/union
+  ]
+
   cppflags += [
     '/DWIN32',
     '/DWINDOWS',
diff --git a/src/include/c.h b/src/include/c.h
index df6c753d908..cc35530424e 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -341,6 +341,16 @@
 #define pg_unreachable() abort()
 #endif
 
+/*
+ * C++ has sligthly different syntax for inline compound literals than C. GCC
+ * and Clang support he C-style syntax too, but MSVC does not.
+ */
+#ifdef __cplusplus
+#define pg_compound_literal(type, ...) (type { __VA_ARGS__ })
+#else
+#define pg_compound_literal(type, ...) ((type) { __VA_ARGS__ })
+#endif
+
 /*
  * Define a compiler-independent macro for determining if an expression is a
  * compile-time integer const.  We don't define this macro to return 0 when
@@ -948,13 +958,17 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName,
 	static_assert(condition, errmessage)
 #define StaticAssertStmt(condition, errmessage) \
 	do { static_assert(condition, errmessage); } while(0)
-#ifdef HAVE_STATEMENT_EXPRESSIONS
+#ifdef __cplusplus
+/* C++11 lambdas provide a convenient way to use static_assert as an expression */
+#define StaticAssertExpr(condition, errmessage) \
+	((void) ([](){ static_assert(condition, errmessage); }(), 0))
+#elif defined(HAVE_STATEMENT_EXPRESSIONS)
 #define StaticAssertExpr(condition, errmessage) \
 	((void) ({ static_assert(condition, errmessage); true; }))
 #else
 #define StaticAssertExpr(condition, errmessage) \
 	((void) sizeof(struct { int static_assert_failure : (condition) ? 1 : -1; }))
-#endif							/* HAVE_STATEMENT_EXPRESSIONS */
+#endif
 
 
 /*
diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h
index ae80975548f..5498d09bcba 100644
--- a/src/include/nodes/pg_list.h
+++ b/src/include/nodes/pg_list.h
@@ -204,10 +204,10 @@ list_length(const List *l)
 /*
  * Convenience macros for building fixed-length lists
  */
-#define list_make_ptr_cell(v)	((ListCell) {.ptr_value = (v)})
-#define list_make_int_cell(v)	((ListCell) {.int_value = (v)})
-#define list_make_oid_cell(v)	((ListCell) {.oid_value = (v)})
-#define list_make_xid_cell(v)	((ListCell) {.xid_value = (v)})
+#define list_make_ptr_cell(v)	pg_compound_literal(ListCell, .ptr_value = (v))
+#define list_make_int_cell(v)	pg_compound_literal(ListCell, .int_value = (v))
+#define list_make_oid_cell(v)	pg_compound_literal(ListCell, .oid_value = (v))
+#define list_make_xid_cell(v)	pg_compound_literal(ListCell, .xid_value = (v))
 
 #define list_make1(x1) \
 	list_make1_impl(T_List, list_make_ptr_cell(x1))
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4c6d56d97d8..92ac0a342b5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
 		  test_bitmapset \
 		  test_bloomfilter \
 		  test_cloexec \
+		  test_cplusplusext \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
 		  test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 1b31c5b98d6..0c7e8ad4856 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -19,6 +19,7 @@ subdir('test_aio')
 subdir('test_binaryheap')
 subdir('test_bitmapset')
 subdir('test_bloomfilter')
+subdir('test_cplusplusext')
 subdir('test_cloexec')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_cplusplusext/Makefile b/src/test/modules/test_cplusplusext/Makefile
new file mode 100644
index 00000000000..19abd0f98c6
--- /dev/null
+++ b/src/test/modules/test_cplusplusext/Makefile
@@ -0,0 +1,24 @@
+# src/test/modules/test_cplusplusext/Makefile
+#
+# Test that PostgreSQL headers compile with a C++ compiler.
+# If this module compiles, the test passes.
+
+MODULE_big = test_cplusplusext
+OBJS = \
+	$(WIN32RES) \
+	test_cplusplusext.o
+PGFILEDESC = "test_cplusplusext - test C++ compatibility of PostgreSQL headers"
+
+# Use C++ compiler for linking because this module includes C++ files
+override COMPILER = $(CXX) $(CXXFLAGS)
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_cplusplusext
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_cplusplusext/meson.build b/src/test/modules/test_cplusplusext/meson.build
new file mode 100644
index 00000000000..d47fbfa7f51
--- /dev/null
+++ b/src/test/modules/test_cplusplusext/meson.build
@@ -0,0 +1,35 @@
+# Copyright (c) 2025-2026, PostgreSQL Global Development Group
+
+# This module tests that PostgreSQL headers compile with a C++ compiler.
+# It has no runtime tests - if it compiles, the test passes.
+
+if not add_languages('cpp', required: false, native: false)
+  subdir_done()
+endif
+
+cpp = meson.get_compiler('cpp')
+
+# MSVC requires C++20 for designated initializers used in PG_MODULE_MAGIC and
+# other macros. Skip if the compiler doesn't support them.
+if not cpp.compiles('''
+    struct Foo { int a; int b; };
+    Foo f = {.a = 1, .b = 2};
+    ''',
+    name: 'C++ designated initializers')
+  subdir_done()
+endif
+
+test_cplusplusext_sources = files(
+  'test_cplusplusext.cpp',
+)
+
+if host_system == 'windows'
+  test_cplusplusext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_cplusplusext',
+    '--FILEDESC', 'test_cplusplusext - test C++ compatibility of PostgreSQL headers',])
+endif
+
+test_cplusplusext = shared_module('test_cplusplusext',
+  test_cplusplusext_sources,
+  kwargs: pg_test_mod_args,
+)
diff --git a/src/test/modules/test_cplusplusext/test_cplusplusext.cpp b/src/test/modules/test_cplusplusext/test_cplusplusext.cpp
new file mode 100644
index 00000000000..012db8b7959
--- /dev/null
+++ b/src/test/modules/test_cplusplusext/test_cplusplusext.cpp
@@ -0,0 +1,51 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_cplusplusext.cpp
+ *		Test that PostgreSQL headers compile with a C++ compiler.
+ *
+ * This file is compiled with a C++ compiler to verify that PostgreSQL
+ * headers remain compatible with C++ extensions.
+ *
+ * Copyright (c) 2025-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_cplusplusext/test_cplusplusext.cpp
+ *
+ * -------------------------------------------------------------------------
+ */
+
+extern "C" {
+#include "postgres.h"
+#include "fmgr.h"
+#include "nodes/pg_list.h"
+#include "nodes/parsenodes.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(test_cplusplus_compat);
+}
+
+StaticAssertDecl(sizeof(int32) == 4, "int32 should be 4 bytes");
+
+extern "C" Datum
+test_cplusplus_compat(PG_FUNCTION_ARGS)
+{
+	List	   *node_list = list_make1(makeNode(RangeTblRef));
+	RangeTblRef *copy = copyObject(linitial_node(RangeTblRef, node_list));
+
+	foreach_ptr(RangeTblRef, rtr, node_list) {
+		rtr->rtindex++;
+	}
+
+	foreach_node(RangeTblRef, rtr, node_list) {
+		rtr->rtindex++;
+	}
+
+	StaticAssertStmt(sizeof(int32) == 4, "int32 should be 4 bytes");
+	(void) StaticAssertExpr(sizeof(int64) == 8, "int64 should be 8 bytes");
+
+	pfree(copy);
+	list_free_deep(node_list);
+
+	PG_RETURN_VOID();
+}
-- 
2.52.0

Reply via email to