While many of the tools I and other developers use making medical devices require C++17 much of the code we write is C++11. Usually this is mandated by the shop. There are good reasons for sticking with C++11. It is easier to change out a library, especially if another project has already vetted that version of the library with the FDA, than it is to change the coding standard of an existing device.
Instead of a full program as many of the other Example/Explanation posts on C++, I’m going to use snippets from my RedDiamond OpenSource project. I have been loath to mentally train myself on many of the newer C++17 features because in my wold I cannot use most of them. That is beginning to change. While lambdas were and are still a damned dangerous C++11 “feature,” some of the other C++17 changes really will make code easier to maintain and safer.
Constructors
Please take a look at the top of this class.
class FileDetails
{
public:
FileDetails();
FileDetails( const FileDetails &other );
FileDetails( const QString fileName );
FileDetails( const QString fileName, const QString cacheFileName, const QString lexerName,
const QString tabText, int eolMode, sptr_t pos );
FileDetails( const QString fileName, const QString cacheFileName, const QString lexerName,
const QString tabText, int eolMode, sptr_t pos, sptr_t lineNumber );
static FileDetails fromString( const QString &str );
FileDetails &operator=( const FileDetails &other );
This is forced on you when you are using a C++11 coding style. Yes, non-static native types like int and those which are synonyms for them like sptr_t could be initialized in the class declaration, but class types could not be. Initializing only some of the variables in the constructor and the rest in the class declaration caused a lot of confusion and coding errors.
Default values are a blessing. Default values are a curse. Many of you would have condensed this down to
class FileDetails
{
public:
FileDetails();
FileDetails( const FileDetails &other );
FileDetails( const QString fileName, const QString cacheFileName="",
const QString lexerName="",
const QString tabText="", int eolMode=0, sptr_t pos=0,
sptr_t lineNumber = 1 );
static FileDetails fromString( const QString &str );
FileDetails &operator=( const FileDetails &other );
but that can get you into a world of hurt.
FileDetails::FileDetails() :
m_lexerName( "null" )
, m_eolMode( SC_EOL_LF )
, m_pos( 0 )
, m_lineNumber( 0 )
, m_lineWrap( SC_WRAP_NONE )
, m_readOnly( false )
, m_isDirty( false )
, m_autoIndentEnabled( false )
{
if ( Overlord::getInstance() != nullptr )
{
m_eolMode = Overlord::getInstance()->platformEOLType();
m_autoIndentEnabled = Overlord::getInstance()->autoIndentEnabled();
}
}
FileDetails::FileDetails( const FileDetails &other ) :
m_fileName( other.m_fileName )
, m_cacheFileName( other.m_cacheFileName )
, m_lexerName( other.m_lexerName )
, m_tabText( other.m_tabText )
, m_eolMode( other.m_eolMode )
, m_pos( other.m_pos )
, m_lineNumber( other.m_lineNumber )
, m_lineWrap( other.m_lineWrap )
, m_readOnly( other.m_readOnly )
, m_isDirty( other.m_isDirty )
, m_autoIndentEnabled( other.m_autoIndentEnabled )
{}
FileDetails::FileDetails( const QString fileName ) :
m_fileName( fileName )
, m_pos( 0 )
, m_lineNumber( 0 )
, m_lineWrap( SC_WRAP_NONE )
, m_readOnly( false )
, m_isDirty( false )
{
m_lexerName = Overlord::getInstance()->getLexerNameForFile( fileName );
m_autoIndentEnabled = Overlord::getInstance()->autoIndentEnabled();
m_eolMode = Overlord::getInstance()->platformEOLType();
}
FileDetails::FileDetails( const QString fileName, const QString cacheFileName, const QString lexerName,
const QString tabText, int eolMode, sptr_t pos ) :
m_fileName( fileName )
, m_cacheFileName( cacheFileName )
, m_lexerName( lexerName )
, m_tabText( tabText )
, m_eolMode( eolMode )
, m_pos( pos )
, m_lineNumber( 0 )
, m_lineWrap( SC_WRAP_NONE )
, m_readOnly( false )
, m_isDirty( false )
{
m_autoIndentEnabled = Overlord::getInstance()->autoIndentEnabled();
}
FileDetails::FileDetails( const QString fileName, const QString cacheFileName, const QString lexerName,
const QString tabText, int eolMode, sptr_t pos, sptr_t lineNumber ) :
m_fileName( fileName )
, m_cacheFileName( cacheFileName )
, m_lexerName( lexerName )
, m_tabText( tabText )
, m_eolMode( eolMode )
, m_pos( pos )
, m_lineNumber( lineNumber )
, m_lineWrap( SC_WRAP_NONE )
, m_readOnly( false )
, m_isDirty( false )
{
m_autoIndentEnabled = Overlord::getInstance()->autoIndentEnabled();
}
You see, many of the default values are controlled by user configuration. If we borrow from our std::optional example and plunge headlong into C++17 we can clean this up quite a bit.
C++17 Version
class FileDetails
{
public:
FileDetails( std::optional<const QString> fileName = std::nullopt,
std::optional<const QString> cacheFileName = std::nullopt,
std::optional<const QString> lexerName = std::nullopt,
std::optional<const QString> tabText = std::nullopt,
std::optional<int> eolMode = std::nullopt,
std::optional<sptr_t> pos = std::nullopt,
std::optional<sptr_t> lineNumber = std::nullopt,
std::optional<int> lineWrap = std::nullopt,
std::optional<bool> readOnly = std::nullopt,
std::optional<bool> autoIndent = std::nullopt,
std::optional<bool> isDirty = std::nullopt );
FileDetails( const FileDetails &other ) = default;
...
private:
// QString initializes itself to empty string
// only have to set the one we need filled in
//
QString m_fileName;
QString m_cacheFileName;
QString m_lexerName {"null"};
QString m_tabText;
int m_eolMode {SC_EOL_LF};
sptr_t m_pos {0};
sptr_t m_lineNumber {0};
int m_lineWrap {SC_WRAP_NONE};
bool m_readOnly {false};
bool m_isDirty {false};
bool m_autoIndentEnabled {false};
};
The = default on the copy constructor is actually part of C++11 it just didn’t work well early on with third party libraries so nobody used it. This definitely won’t work with more complex classes in Qt, CopperSpice, etc. which deliberately disable copy constructors. Has to do with dynamic memory allocation and reference counting and other yadda yadda.
We now have only one constructor. Every parameter is optional. I even added some that weren’t on the other constructors. Some of you won’t like the following coding style, I don’t care. I use the Barr Group coding standard for medical device work and you never have a naked if statement.
FileDetails::FileDetails(
std::optional<const QString> fileName,
std::optional<const QString> cacheFileName,
std::optional<const QString> lexerName,
std::optional<const QString> tabText,
std::optional<int> eolMode,
std::optional<sptr_t> pos,
std::optional<sptr_t> lineNumber,
std::optional<int> lineWrap,
std::optional<bool> readOnly,
std::optional<bool> autoIndent,
std::optional<bool> isDirty )
{
if ( fileName.has_value() )
{
m_fileName = fileName.value();
}
if ( cacheFileName.has_value() )
{
m_cacheFileName = cacheFileName.value();
}
if ( lexerName.has_value() )
{
m_lexerName = lexerName.value();
}
else
{
// a default construction will not have a file name so we
// only override default value if file name provided.
if ( m_fileName.length() > 0 )
{
m_lexerName = Overlord::getInstance()->getLexerNameForFile( m_fileName );
}
}
if ( tabText.has_value() )
{
m_tabText = tabText.value();
}
if ( eolMode.has_value() )
{
m_eolMode = eolMode.value();
}
else
{
m_eolMode = Overlord::getInstance()->platformEOLType();
}
if ( pos.has_value() )
{
m_pos = pos.value();
}
if ( lineNumber.has_value() )
{
m_lineNumber = lineNumber.value();
}
if ( lineWrap.has_value() )
{
m_lineWrap = lineWrap.value();
}
if ( readOnly.has_value() )
{
m_readOnly = readOnly.value();
}
if ( autoIndent.has_value() )
{
m_autoIndentEnabled = autoIndent.value();
}
else
{
m_autoIndentEnabled = Overlord::getInstance()->autoIndentEnabled();
}
if ( isDirty.has_value() )
{
m_isDirty = isDirty.value();
}
}
Previously we had to have multiple constructors which is a maintenance time bomb. I’ve had to deal with that at client sites. Classes with six or more constructors because they had been using C++98 or C++11 standard and now you need to add a field. Always one that gets missed. Leads to very difficult to track down bugs.
This is much more maintainable. Anything that could have a hard coded default value has it assigned in the header file. Use of std::optional allows us to fill in as many parameters as we wish. Default values coming from the user configuration are pulled in as needed.