AppEnum: Type-erase the mapping machinery into a non-template base

Move the loops, lookups, and QString machinery out of
AppEnum<T>::EnumMapper into a new non-template AppEnumMapperBase defined
in cafAppEnum.cpp. Each member is now compiled once for the whole
program rather than once per enum type, cutting build time and binary
size for a class template instantiated across hundreds of enums.

AppEnum<T> becomes a thin static_cast shim around the base methods.
Enum values are stored as int in the mapping; a static_assert rejects
enum types whose underlying type is wider than int. The widest
underlying type currently in use in ResInsight is short, so all current
instantiations are unaffected.

Use std::optional<int> for the mapper default value, and assert against
an empty mapping in defaultValue() (the prior code returned m_mapping[0]
unconditionally, which is undefined behavior on empty).

Document why mapper() uses an explicit init flag rather than
std::call_once or a magic-static lambda: setUp() re-enters mapper() via
addItem, so anything that locks on first init would deadlock.
First-access must therefore be single-threaded, as already exercised by
BaseTest.CreateObjectInMultipleThreads.

Add AppEnumTest.MappingRoundTrip covering text <-> enum, index <-> enum,
isValid, and the failure paths of setFromText / setFromIndex (resets to
default, returns false).
This commit is contained in:
Magne Sjaastad committed 2026-05-05 14:27:12 +02:00
1 parent e8db66a377
commit 7fcd192b71
4 files changed
+331 -206

No files matched your search

@@ -17,6 +17,7 @@ qt_standard_project_setup()
set(PROJECT_FILES
cafAssert.h
cafAppEnum.h
cafAppEnum.cpp
cafClassTypeName.h
cafPdmBase.h
cafPdmChildArrayField.h
@@ -0,0 +1,198 @@
//##################################################################################################
//
// Custom Visualization Core library
// Copyright (C) 2011-2013 Ceetron AS
//
// This library may be used under the terms of either the GNU General Public License or
// the GNU Lesser General Public License as follows:
//
// GNU General Public License Usage
// This library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <<http://www.gnu.org/licenses/gpl.html>>
// for more details.
//
// GNU Lesser General Public License Usage
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU Lesser General Public License at <<http://www.gnu.org/licenses/lgpl-2.1.html>>
// for more details.
//
//##################################################################################################
// AppEnumMapperBase holds the type-erased mapping (int <-> text/uiText/aliases) shared by every
// AppEnum<T> instantiation. Defining its members in this translation unit instead of inline in the
// class template means each lookup and QString helper is compiled once for the whole program rather
// than once per enum type, which keeps build times and binary size in check.
#include "cafAppEnum.h"
#include "cafAssert.h"
namespace caf
{
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool AppEnumMapperBase::EnumData::isMatching( const QString& text ) const
{
return text == m_text || m_aliases.contains( text );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void AppEnumMapperBase::addItem( int enumVal, const QString& text, QString uiText, const QStringList& aliases )
{
// Make sure the alias text is unique for enum
for ( const auto& alias : aliases )
{
for ( const auto& enumData : m_mapping )
{
CAF_ASSERT( !enumData.isMatching( alias ) );
}
}
// Make sure the text is trimmed, as this text is streamed to XML and will be trimmed when read back
// from XML text https://github.com/OPM/ResInsight/issues/7829
m_mapping.push_back( { enumVal, text.trimmed(), uiText, aliases } );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void AppEnumMapperBase::setDefault( int defaultEnumValue )
{
m_defaultValue = defaultEnumValue;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int AppEnumMapperBase::defaultValue() const
{
if ( m_defaultValue.has_value() ) return *m_defaultValue;
CAF_ASSERT( !m_mapping.empty() );
return m_mapping[0].m_enumVal;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool AppEnumMapperBase::isValid( const QString& text ) const
{
for ( size_t idx = 0; idx < m_mapping.size(); ++idx )
{
if ( text == m_mapping[idx].m_text ) return true;
}
return false;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
size_t AppEnumMapperBase::size() const
{
return m_mapping.size();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool AppEnumMapperBase::enumVal( int& value, const QString& text ) const
{
value = defaultValue();
QString trimmedText = text.trimmed();
for ( size_t idx = 0; idx < m_mapping.size(); ++idx )
{
if ( m_mapping[idx].isMatching( trimmedText ) )
{
value = m_mapping[idx].m_enumVal;
return true;
}
}
return false;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool AppEnumMapperBase::enumVal( int& value, size_t index ) const
{
value = defaultValue();
if ( index < m_mapping.size() )
{
value = m_mapping[index].m_enumVal;
return true;
}
return false;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
size_t AppEnumMapperBase::index( int enumValue ) const
{
size_t idx;
for ( idx = 0; idx < m_mapping.size(); ++idx )
{
if ( enumValue == m_mapping[idx].m_enumVal ) return idx;
}
return idx; // returns size() if not found, matching prior behavior
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString AppEnumMapperBase::uiText( int value ) const
{
for ( size_t idx = 0; idx < m_mapping.size(); ++idx )
{
if ( value == m_mapping[idx].m_enumVal ) return m_mapping[idx].m_uiText;
}
return "";
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QStringList AppEnumMapperBase::uiTexts() const
{
QStringList uiTextList;
for ( size_t idx = 0; idx < m_mapping.size(); ++idx )
{
uiTextList.append( m_mapping[idx].m_uiText );
}
return uiTextList;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString AppEnumMapperBase::text( int value ) const
{
for ( size_t idx = 0; idx < m_mapping.size(); ++idx )
{
if ( value == m_mapping[idx].m_enumVal ) return m_mapping[idx].m_text;
}
return "";
}
} // namespace caf
@@ -36,13 +36,15 @@
#pragma once
#include "cafAssert.h"
#include "cafPdmFieldHandle.h"
#include "cafPdmFieldTraits.h"
#include <QString>
#include <QStringList>
#include <map>
#include <optional>
#include <type_traits>
#include <vector>
namespace caf
@@ -119,11 +121,50 @@ public:
virtual void setTextForSerialization( const QString& text ) = 0;
};
//==================================================================================================
/// Non-template backing store for AppEnum<T>. Holds the (text, uiText, aliases) mapping keyed by
/// the enum value cast to int. By keeping all loops, lookups, and QString machinery in this
/// non-template class, every member is compiled exactly once instead of once per enum type.
/// AppEnum<T> reduces to a thin static_cast shim around the relevant methods.
//==================================================================================================
class AppEnumMapperBase
{
public:
void addItem( int enumVal, const QString& text, QString uiText, const QStringList& aliases );
void setDefault( int defaultEnumValue );
int defaultValue() const;
bool isValid( const QString& text ) const;
size_t size() const;
bool enumVal( int& value, const QString& text ) const;
bool enumVal( int& value, size_t index ) const;
size_t index( int enumValue ) const;
QString uiText( int value ) const;
QStringList uiTexts() const;
QString text( int value ) const;
private:
struct EnumData
{
int m_enumVal;
QString m_text;
QString m_uiText;
QStringList m_aliases;
bool isMatching( const QString& text ) const;
};
std::vector<EnumData> m_mapping;
std::optional<int> m_defaultValue;
};
template <class T>
class AppEnum : public AppEnumInterface
{
static_assert( std::is_enum_v<T>, "AppEnum<T> requires T to be an enum type." );
static_assert( sizeof( std::underlying_type_t<T> ) <= sizeof( int ),
"AppEnum stores enum values as int; T's underlying type is wider than int." );
public:
AppEnum() { m_value = EnumMapper::instance()->defaultValue(); }
AppEnum() { m_value = static_cast<T>( mapper().defaultValue() ); }
AppEnum( T value )
: m_value( value )
{
@@ -160,44 +201,56 @@ public:
operator T() const { return m_value; }
T value() const { return m_value; }
size_t index() const { return EnumMapper::instance()->index( m_value ); }
QString text() const { return EnumMapper::instance()->text( m_value ); }
QString uiText() const { return EnumMapper::instance()->uiText( m_value ); }
size_t index() const { return mapper().index( static_cast<int>( m_value ) ); }
QString text() const { return mapper().text( static_cast<int>( m_value ) ); }
QString uiText() const { return mapper().uiText( static_cast<int>( m_value ) ); }
AppEnum& operator=( T value )
{
m_value = value;
return *this;
}
bool setFromText( const QString& text ) { return EnumMapper::instance()->enumVal( m_value, text ); }
bool setFromIndex( size_t index ) { return EnumMapper::instance()->enumVal( m_value, index ); }
bool setFromText( const QString& text )
{
int v = 0;
bool ok = mapper().enumVal( v, text );
m_value = static_cast<T>( v );
return ok;
}
bool setFromIndex( size_t index )
{
int v = 0;
bool ok = mapper().enumVal( v, index );
m_value = static_cast<T>( v );
return ok;
}
QString textForSerialization() const override { return text(); }
void setTextForSerialization( const QString& text ) override { setFromText( text ); }
// Static interface to access the properties of the enum definition
static bool isValid( const QString& text ) { return EnumMapper::instance()->isValid( text ); }
static bool isValid( size_t index ) { return index < EnumMapper::instance()->size(); }
static size_t size() { return EnumMapper::instance()->size(); }
static bool isValid( const QString& text ) { return mapper().isValid( text ); }
static bool isValid( size_t index ) { return index < mapper().size(); }
static size_t size() { return mapper().size(); }
static QStringList uiTexts() { return EnumMapper::instance()->uiTexts(); }
static QStringList uiTexts() { return mapper().uiTexts(); }
static T fromIndex( size_t idx )
{
T val;
EnumMapper::instance()->enumVal( val, idx );
return val;
int v = 0;
mapper().enumVal( v, idx );
return static_cast<T>( v );
}
static T fromText( const QString& text )
{
T val;
EnumMapper::instance()->enumVal( val, text );
return val;
int v = 0;
mapper().enumVal( v, text );
return static_cast<T>( v );
}
static size_t index( T enumValue ) { return EnumMapper::instance()->index( enumValue ); }
static QString text( T enumValue ) { return EnumMapper::instance()->text( enumValue ); }
static size_t index( T enumValue ) { return mapper().index( static_cast<int>( enumValue ) ); }
static QString text( T enumValue ) { return mapper().text( static_cast<int>( enumValue ) ); }
static QString textFromIndex( size_t idx ) { return text( fromIndex( idx ) ); }
static QString uiText( T enumValue ) { return EnumMapper::instance()->uiText( enumValue ); }
static QString uiText( T enumValue ) { return mapper().uiText( static_cast<int>( enumValue ) ); }
static QString uiTextFromIndex( size_t idx ) { return uiText( fromIndex( idx ) ); }
private:
@@ -209,203 +262,39 @@ private:
static void setUp();
static void addItem( T enumVal, const QString& text, const QString& uiText, const QStringList& aliases = {} )
{
EnumMapper::instance()->addItem( enumVal, text, uiText, aliases );
mapper().addItem( static_cast<int>( enumVal ), text, uiText, aliases );
}
static void setDefault( T defaultEnumValue ) { EnumMapper::instance()->setDefault( defaultEnumValue ); }
static void setDefault( T defaultEnumValue ) { mapper().setDefault( static_cast<int>( defaultEnumValue ) ); }
// Per-T storage for the mapper. The base class methods are non-template so they're compiled
// once total; only this accessor and the trivial static_cast shims above are instantiated per T.
//
// The explicit isInitialized flag (rather than e.g. std::call_once or a magic-static lambda) is
// deliberate: setUp() invokes addItem(), which calls mapper() recursively. Setting the flag
// before invoking setUp() lets the re-entrant calls return the storedInstance instead of
// deadlocking. First-access must therefore happen single-threaded; see
// CreateObjectInMultipleThreads in cafPdmBasicTest.cpp.
static AppEnumMapperBase& mapper()
{
static AppEnumMapperBase storedInstance;
static bool isInitialized = false;
if ( !isInitialized )
{
isInitialized = true;
AppEnum<T>::setUp();
}
return storedInstance;
}
T m_value;
static std::map<QString, std::vector<T>> m_enumSubset; // Key format: "ownerClass::fieldKeyword"
//==================================================================================================
/// A private class to handle the instance of the mapping vector.
/// all access methods could have been placed directly in the \class AppEnum class,
/// but AppEnum implementation gets nicer this way.
/// The real core of this class is the vector map member and the static instance method
//==================================================================================================
class EnumMapper
{
private:
class EnumData
{
public:
EnumData( T enumVal, const QString& text, const QString& uiText, const QStringList& aliases )
: m_enumVal( enumVal )
, m_text( text )
, m_uiText( uiText )
, m_aliases( aliases )
{
}
bool isMatching( const QString& text ) const { return ( text == m_text || m_aliases.contains( text ) ); }
T m_enumVal;
QString m_text;
QString m_uiText;
QStringList m_aliases;
};
public:
void addItem( T enumVal, const QString& text, QString uiText, const QStringList& aliases )
{
// Make sure the alias text is unique for enum
for ( const auto& alias : aliases )
{
for ( const auto& enumData : instance()->m_mapping )
{
CAF_ASSERT( !enumData.isMatching( alias ) );
}
}
// Make sure the text is trimmed, as this text is streamed to XML and will be trimmed when read back
// from XML text https://github.com/OPM/ResInsight/issues/7829
instance()->m_mapping.push_back( EnumData( enumVal, text.trimmed(), uiText, aliases ) );
}
static EnumMapper* instance()
{
static EnumMapper storedInstance;
static bool isInitialized = false;
if ( !isInitialized )
{
isInitialized = true;
AppEnum<T>::setUp();
}
return &storedInstance;
}
void setDefault( T defaultEnumValue )
{
m_defaultValue = defaultEnumValue;
m_defaultValueIsSet = true;
}
T defaultValue() const
{
if ( m_defaultValueIsSet )
{
return m_defaultValue;
}
else
{
// CAF_ASSERT(m_mapping.size());
return m_mapping[0].m_enumVal;
}
}
bool isValid( const QString& text ) const
{
size_t idx;
for ( idx = 0; idx < m_mapping.size(); ++idx )
{
if ( text == m_mapping[idx].m_text ) return true;
}
return false;
}
size_t size() const { return m_mapping.size(); }
bool enumVal( T& value, const QString& text ) const
{
value = defaultValue();
QString trimmedText = text.trimmed();
for ( size_t idx = 0; idx < m_mapping.size(); ++idx )
{
// Make sure the text parsed from a text stream is trimmed
// https://github.com/OPM/ResInsight/issues/7829
if ( m_mapping[idx].isMatching( trimmedText ) )
{
value = m_mapping[idx].m_enumVal;
return true;
}
}
return false;
}
bool enumVal( T& value, size_t index ) const
{
value = defaultValue();
if ( index < m_mapping.size() )
{
value = m_mapping[index].m_enumVal;
return true;
}
else
return false;
}
size_t index( T enumValue ) const
{
size_t idx;
for ( idx = 0; idx < m_mapping.size(); ++idx )
{
if ( enumValue == m_mapping[idx].m_enumVal ) return idx;
}
return idx;
}
QString uiText( T value ) const
{
size_t idx;
for ( idx = 0; idx < m_mapping.size(); ++idx )
{
if ( value == m_mapping[idx].m_enumVal ) return m_mapping[idx].m_uiText;
}
return "";
}
QStringList uiTexts() const
{
QStringList uiTextList;
size_t idx;
for ( idx = 0; idx < m_mapping.size(); ++idx )
{
uiTextList.append( m_mapping[idx].m_uiText );
}
return uiTextList;
}
QString text( T value ) const
{
size_t idx;
for ( idx = 0; idx < m_mapping.size(); ++idx )
{
if ( value == m_mapping[idx].m_enumVal ) return m_mapping[idx].m_text;
}
return "";
}
private:
EnumMapper()
: m_defaultValue( T() )
, m_defaultValueIsSet( false )
{
}
friend class AppEnum<T>;
std::vector<EnumData> m_mapping;
T m_defaultValue;
bool m_defaultValueIsSet;
};
};
template <class T>
std::map<QString, std::vector<T>> AppEnum<T>::m_enumSubset;
} // namespace caf
#include "cafPdmFieldTraits.h"
namespace caf
{
template <typename T>
QVariant pdmToVariant( const AppEnum<T>& value )
{
@@ -70,6 +70,43 @@ public:
caf::PdmDataValueField<caf::AppEnum<TestEnumType>> m_enumField;
};
//--------------------------------------------------------------------------------------------------
/// Round-trip exercise of the type-erased mapping in AppEnumMapperBase: text <-> enum, index
/// <-> enum, and out-of-range / unknown lookups.
//--------------------------------------------------------------------------------------------------
TEST( AppEnumTest, MappingRoundTrip )
{
using AE = caf::AppEnum<TestEnumType>;
EXPECT_EQ( 5u, AE::size() );
EXPECT_EQ( QString( "VALUE_C" ), AE::text( TestEnumType::VALUE_C ) );
EXPECT_EQ( QString( "Value C" ), AE::uiText( TestEnumType::VALUE_C ) );
EXPECT_EQ( 2u, AE::index( TestEnumType::VALUE_C ) );
EXPECT_EQ( TestEnumType::VALUE_D, AE::fromText( "VALUE_D" ) );
EXPECT_EQ( TestEnumType::VALUE_D, AE::fromIndex( 3 ) );
EXPECT_TRUE( AE::isValid( QString( "VALUE_A" ) ) );
EXPECT_FALSE( AE::isValid( QString( "NOT_A_VALUE" ) ) );
EXPECT_TRUE( AE::isValid( size_t{ 0 } ) );
EXPECT_FALSE( AE::isValid( size_t{ 99 } ) );
AE e;
EXPECT_EQ( TestEnumType::VALUE_A, e.value() ); // Default is VALUE_A
EXPECT_TRUE( e.setFromText( "VALUE_E" ) );
EXPECT_EQ( TestEnumType::VALUE_E, e.value() );
EXPECT_TRUE( e.setFromIndex( 1 ) );
EXPECT_EQ( TestEnumType::VALUE_B, e.value() );
// Unknown text and out-of-range index return false and reset to default
EXPECT_FALSE( e.setFromText( "DOES_NOT_EXIST" ) );
EXPECT_EQ( TestEnumType::VALUE_A, e.value() );
e = TestEnumType::VALUE_C;
EXPECT_FALSE( e.setFromIndex( 99 ) );
EXPECT_EQ( TestEnumType::VALUE_A, e.value() );
}
//--------------------------------------------------------------------------------------------------
/// Test that two different objects can have the same field keyword with different enum subsets
//--------------------------------------------------------------------------------------------------