diff --git a/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs
index 95daef9..a5ae01d 100644
--- a/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs
+++ b/Coder.Test/Languages/CppGeneratedSourceCompilesTests.cs
@@ -128,6 +128,54 @@ public void AMutableStaticFieldCompilesInsideAClass()
});
}
+ ///
+ /// Tests that a constant string longer than the small-string buffer compiles in each position a
+ /// constant may stand.
+ ///
+ ///
+ /// constexpr std::string compiles only while the value fits libstdc++'s small-string buffer,
+ /// so a short literal would pass whichever way the generator spelled it. The value here is longer
+ /// than that buffer, which is what makes the spelling load-bearing.
+ ///
+ [TestMethod]
+ public void ALongConstantStringCompilesWhereverAConstantMayStand()
+ {
+ string? compiler = ToolchainHarness.FindOnPath("--version", Compilers);
+ if (compiler is null)
+ {
+ Assert.Inconclusive("No C++ compiler on the path, so nothing was compiled.");
+ return;
+ }
+
+ const string Long = "a string longer than fifteen chars";
+
+ SourceFile file = new("strings") { IsHeader = true, Imports = { "" } };
+ file.Members.Add(new FieldDeclaration("GREETING", "string") { IsConstant = true, InitialValue = Literal.Text(Long) });
+
+ ClassDeclaration holder = new("Names") { Kind = TypeDeclarationKind.Struct };
+ holder.Members.Add(new FieldDeclaration("Name", "string") { IsStatic = true, IsConstant = true, InitialValue = Literal.Text(Long) });
+ file.Members.Add(holder);
+
+ ClassDeclaration labels = new("Labels");
+ labels.Members.Add(new VariableDeclaration("Title", "string") { Visibility = Visibility.Public, IsConstant = true, InitialValue = Literal.Text(Long) });
+ file.Members.Add(labels);
+
+ ToolchainHarness.InTemporaryDirectory(directory =>
+ {
+ File.WriteAllText(Path.Combine(directory, "strings.h"), new CppGenerator().Generate(file));
+ File.WriteAllText(
+ Path.Combine(directory, "driver.cpp"),
+ "#include \"strings.h\"\n\nint main()\n{\n\treturn static_cast(GREETING.size() + Names::Name.size() + Labels::Title.size()) * 0;\n}\n");
+
+ (int exitCode, string output) = ToolchainHarness.Run(
+ compiler,
+ "-std=c++20 -Wall -Wextra -pedantic -c driver.cpp -o driver.o",
+ directory);
+
+ Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated header:{Environment.NewLine}{output}");
+ });
+ }
+
///
/// Builds a header declaring an array in each position one with no bound may stand in.
///
diff --git a/Coder.Test/Languages/CppGeneratorTests.cs b/Coder.Test/Languages/CppGeneratorTests.cs
index 260cde0..6885cce 100644
--- a/Coder.Test/Languages/CppGeneratorTests.cs
+++ b/Coder.Test/Languages/CppGeneratorTests.cs
@@ -115,6 +115,31 @@ public void ConstantDeclaration_IsQualified()
Assert.AreEqual($"const int limit = 10;{CodeBlocker.DefaultNewLineString}", Generator.Generate(constant));
}
+ ///
+ /// Tests that a constant field of a type that allocates is const rather than constexpr,
+ /// and that a literal type keeps constexpr.
+ ///
+ [TestMethod]
+ public void ConstantField_OfANonLiteralType_IsConstRatherThanConstexpr()
+ {
+ FieldDeclaration text = new("Name", "string") { IsConstant = true, InitialValue = Literal.Text("a string longer than fifteen chars") };
+ FieldDeclaration number = new("Limit", "int") { IsConstant = true, InitialValue = Literal.Number(10) };
+
+ Assert.StartsWith("inline const std::string Name = ", Generator.Generate(text), StringComparison.Ordinal);
+ Assert.StartsWith("inline constexpr int Limit = ", Generator.Generate(number), StringComparison.Ordinal);
+
+ ClassDeclaration holder = new("Names") { Kind = TypeDeclarationKind.Struct };
+ holder.Members.Add(text);
+ holder.Members.Add(number);
+ holder.Members.Add(new VariableDeclaration("Title", "string", Literal.Text("a title")) { IsConstant = true });
+ string code = Generator.Generate(holder);
+
+ Assert.Contains("inline static const std::string Name = ", code, StringComparison.Ordinal);
+ Assert.Contains("static constexpr int Limit = ", code, StringComparison.Ordinal);
+ Assert.Contains("inline static const std::string Title = ", code, StringComparison.Ordinal);
+ Assert.DoesNotContain("constexpr std::string", code, StringComparison.Ordinal);
+ }
+
///
/// Tests that logical operators use the C++ symbolic spellings rather than Python's words.
///
diff --git a/Coder/Languages/CppGenerator.cs b/Coder/Languages/CppGenerator.cs
index 21b4d98..cdcd970 100644
--- a/Coder/Languages/CppGenerator.cs
+++ b/Coder/Languages/CppGenerator.cs
@@ -57,6 +57,17 @@ public class CppGenerator : CFamilyGenerator
{ "object", "std::any" }
};
+ ///
+ /// The standard-library types this generator writes that are not literal types.
+ ///
+ private static readonly HashSet NonLiteralTypes = new(StringComparer.Ordinal)
+ {
+ "std::string",
+ "std::vector",
+ "std::map",
+ "std::any",
+ };
+
///
/// Gets the unique identifier for this language generator.
///
@@ -607,13 +618,15 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo
/// A constant member is emitted as static constexpr. A plain const member is a
/// per-instance value initialised once per object, which is not what a constant means; the
/// static constexpr spelling is the one that gives the class a single compile-time value,
- /// and it is available because a constant declared here is initialised from a literal.
+ /// and it is available because a constant declared here is initialised from a literal. A type
+ /// that is not a literal type is inline static const instead; see .
///
private void GenerateField(VariableDeclaration field, CodeBlocker code)
{
if (field.IsConstant && field.InitialValue is not null)
{
- code.Write("static constexpr ");
+ bool literal = field.IsTypeInferred || field.Type is null || IsLiteralType(field.Type);
+ code.Write(literal ? "static constexpr " : "inline static const ");
}
else if (field.IsConstant)
{
@@ -786,11 +799,20 @@ protected override string SpellType(TypeReference type)
/// inline, so saying it inside a class would be noise at best. is what
/// tells the two apart, and it is a depth rather than a flag so a type nested in a type stays
/// balanced.
+ ///
+ /// A constant of a type that is not a literal type is const rather than constexpr;
+ /// see .
+ ///
///
private string SpellStorage(FieldDeclaration field)
{
if (field.IsConstant)
{
+ if (field.Type is not null && !IsLiteralType(field.Type))
+ {
+ return insideType > 0 ? "inline static const " : "inline const ";
+ }
+
return insideType > 0 ? "static constexpr " : "inline constexpr ";
}
@@ -804,6 +826,30 @@ private string SpellStorage(FieldDeclaration field)
return string.Empty;
}
+ ///
+ /// Reports whether a type can be declared constexpr.
+ ///
+ /// The declared type.
+ /// False for a standard-library type that allocates; otherwise true.
+ ///
+ /// std::string and the containers allocate, and an allocation cannot outlive the constant
+ /// evaluation that made it, so a constexpr one does not compile. A short string appears to
+ /// work only because it fits libstdc++'s small-string buffer, so whether the output builds would
+ /// otherwise depend on the literal's length and the standard library. Before C++20 it is never
+ /// valid. A pointer or reference to one is a literal type, and a type this generator does not map
+ /// is the caller's, which is assumed to have been written to be one.
+ ///
+ private static bool IsLiteralType(TypeReference type)
+ {
+ if (type.Indirection != TypeIndirection.None)
+ {
+ return true;
+ }
+
+ string name = TypeMappings.TryGetValue(type.Name, out string? mapped) ? mapped : type.Name;
+ return !NonLiteralTypes.Contains(name);
+ }
+
///
/// Spells a type's argument list, supplying the one a bare container does not name.
///