Use invalid defaults for EvenID parameter inits
Since zero can be a valid number for various EventID related parameters, eg run# or event#, we need invalid defaults to be able to distinguish between EventIDs that are set with lumiblock info, vs run# info
Merge request reports
Activity
9 9 10 10 #include "GaudiKernel/EventIDBase.h" 11 11 12 #include <limits> 13 14 const EventIDBase::number_type EventIDBase::UNDEFNUM = 15 std::numeric_limits<EventIDBase::number_type>::max(); 16 const EventIDBase::event_number_t EventIDBase::UNDEFEVT = 17 std::numeric_limits<EventIDBase::event_number_t>::max(); 18 Can I suggest to keep the 'special' value in the definition of a small dedicated class, i.e. something along the lines of:
template <typename T, T empty_value> class compact_optional { T m_val; public: constexpr compact_optional() : m_val{empty_value} {} constexpr compact_optional(const T& v) : m_val{v} {} constexpr compact_optional(T&& v) : m_val{std::move(v)} {} constexpr explicit operator bool() const noexcept { return m_val!=empty_value; } const T& value() const noexcept { if (UNLIKELY(!bool(*this))) throw std::logic_error("attempt to access empty compact_optional" ); return m_val; } constexpr const T& operator*() const { return m_val; } compact_optional& operator=(const T& v) { m_val = v; return *this; } compact_optional& operator=(T&& v) { m_val = std::move(v); return *this; } compact_optional& reset() { m_val = empty_value; return *this; } }; using event_number_t = compact_optional< uint64_t, std::numeric_limits<uint64_t>::max()>; using number_type = compact_optional< unsigned int, std::numeric_limits<unsigned_int>::max()>;
Doing it this way avoids anyone having to know the actual 'special invalid' value : it can be hidden in a
using
statement which would replace thetypedef
...Adding a small utility class like the above will make it easier to re-use, as I'm sure this will be useful elsewhere.
(note: the above is adapted from https://akrzemi1.wordpress.com/2015/07/15/efficient-optional-values)
Edited by Gerhard RavenJust specify the default argument as
= { }
or= event_number_t{}
(i.e. instead of where there would now be= INVALID
) in which case the default constructor is called, which sets it to the invalid state ;-) . So there is no need for more constructors (unless I misinterpret your question).Edited by Gerhard Ravenempirical evidence indicates the opposite on both counts.
the only way to initialize the lumi_block with an invalid value is to expose the compact_optional in the constructor. ie if we do this:
typedef unsigned int num_t; typedef compact_optional<num_t, std::numeric_limits<num_t>::max()> co_t; class Test { public: Test (num_t i={}) : m_i(i) {}; void prt() const { std::cout << "valid: " << m_i.valid() << " empty: " << m_i.empty(); if ( m_i.valid() ) std::cout << " val: " << m_i.value(); std::cout << std::endl; } private: co_t m_i = {}; }; main() { Test a1(1), a2(0), a3; a1.prt(); a2.prt(); a3.prt(); }
(I added an
empty()
method to compact_optional to print out the empty_value.) executing this will give:valid: 1 empty: 4294967295 val: 1 valid: 1 empty: 4294967295 val: 0 valid: 1 empty: 4294967295 val: 0
notice how
a3
is valid, with a value of0
, which is not what we want. if we change the constructor to:Test(co_t i={}) : m_i(i) {}
then we get what we want
valid: 1 empty: 4294967295 val: 1 valid: 1 empty: 4294967295 val: 0 valid: 0 empty: 4294967295
however we've now exposed the
compact_optional
in the interface, which we don't want.furthermore, if we change the empty_value to
typedef compact_optional<num_t, ~std::numeric_limits<num_t>::max()> co_t;
then the result is:
valid: 1 empty: 0 val: 1 valid: 0 empty: 0 valid: 0 empty: 0
ie, the value of
empty_value
is0
, notstd::numeric_limits::max()
Edited by Charles LeggettThis:
Test (num_t i={}) : m_i(i) {};
does not default-construct
m_i
. You are default-constructing anum_t
, which yields a zero-valuedi
, and you are using thisi
to call the constructor of thecompact_optional<num_t,...>
, which then becomes valid, with the same value asi
, i.e. 0..The solution is to not provide a default value, and instead add a default constructor for
Test
, i.e.Test (num_t i) : m_i(i) {}; Test() = default;
And then you get:
#include <utility> #include <iostream> #include <limits> template <typename T, T empty_value> class compact_optional { T m_val; public: constexpr compact_optional() : m_val{empty_value} {} constexpr compact_optional(const T& v) : m_val{v} {} constexpr compact_optional(T&& v) : m_val{std::move(v)} {} constexpr explicit operator bool() const noexcept { return m_val!=empty_value; } const T& value() const { if (!*this) throw std::logic_error("attempt to access empty compact_optional" ); return m_val; } constexpr const T& operator*() const { return m_val; } compact_optional& operator=(const T& v) { m_val = v; return *this; } compact_optional& operator=(T&& v) { m_val = std::move(v); return *this; } compact_optional& reset() { m_val = empty_value; return *this; } }; typedef unsigned int num_t; typedef compact_optional<num_t, std::numeric_limits<num_t>::max()> co_t; class Test { public: Test () = default; Test (num_t i) : m_i(i) {}; void prt() const { std::cout << "valid: " << std::boolalpha << bool(m_i) ; if ( m_i ) std::cout << " val: " << m_i.value(); std::cout << std::endl; } private: co_t m_i = {}; }; int main() { Test a1(1), a2(0), a3; a1.prt(); a2.prt(); a3.prt(); }
which prints -- see http://coliru.stacked-crooked.com/a/d3041788e0994fc7 -- as expected:
valid: true val: 1 valid: true val: 0 valid: false
Note that, if you do not want 'user code' to know about compact_optional, you have to expose the invalid value, as how would a 'user' ever know (s)he got an invalid value?
Edited by Gerhard Ravenyes, that's precisely what i wrote a few messages back, and could not understand how you were claiming otherwise. so now we need multiple constructors:
EventIDBase() = default; EventIDBase(run_number, event_number); EventIDBase(run_number, event_number, time_stamp, time_stamp_ns=0); EventIDBase(run_number, event_number, time_stamp, time_stamp_ns, lumi_block, bunch_crossing_id=0); private: co_t m_run_number = {}; co_t m_event_number = {}; co_t m_time_stamp = {}; num_t m_time_stamp_ns {0}; co_t m_lumi_block = {}; num_t m_bunch_crossing_id {0};
i think you were writing about setting the default value in the declaration of the member variables, whereas i was referring to the default arguments in the constructor.
as for knowing about valid/invalid, the user needs to test
isRunEvent()
,isLumiEvent()
, andisTimeStamp()
Because what I had in mind is to use the
conditional_optional
in the interface -- including as constructor arguments, i.e. what I had in mind wasusing event_number_t =compact_optional<uint 64,~0>
(as I had in the initial code snippet) and nottypedef uint64 event_number_t
....Now, the other option, if you really want to keep the outside interface the same, is to still define
UNDEFEVT
as~0u
, and use that in the definition of the optional i.e.compact_optional<event_number_t, UNDEFEVT>
, and useUNDEFEVT
as default constructor argument. At that point thecompact_optional
is only ever used internally as an implementation detail.A third option is to actually allow for a piece-meal migration by adding an implicit conversion operator from
compact_optional<T,empty_val>
toT
: code that wants acompact_optional<T,empty_val>
as argument would require one 'by value' in which case, when invoked with aT
, the constructor fromT
is called -- and if thatT
happens to be anempty_val
value, it will construct an empty optional --, and code which wants aT
and is invoked with acompact_optional<T,empty_val>
will invoke the implicit conversion -- and as long as the conversion of emptycompact_optional
to aT
yields anempty_val
, nothing changes. So this would provide full interoperability between 'old' and 'new' code. And the implicit conversion could be flagged asdeprecated
to encourage people to move tocompact_optional
natively.
Added 1 commit:
- 1a071f04 - used compact_optional<T> to handle invalid default initialziers
52 number_type time_stamp, 53 number_type time_stamp_ns_offset, 54 number_type lumi_block, 48 55 number_type bunch_crossing_id=0); 56 49 57 // Use default copy constructor. 50 58 virtual ~EventIDBase(); 51 59 //@} 52 60 53 61 /// run number - 32 bit unsigned 54 number_type run_number () const { return m_run_number; } 62 number_type run_number () const { return m_run_number.value(); } 55 63 56 64 /// event number - 64 bit unsigned 57 event_number_t event_number () const { return m_event_number; } 65 event_number_t event_number () const { return m_event_number.value(); } I'd go one step further, and write the 'interface' in terms of compact_optional. Because what happens if the event number is 'invalid'? This would return the 'special invalid' values, at which point the caller would have to know this. So I'd just return 'm_event_number' as is, including its type, and allow the caller to use it as such.
mentioned in commit lhcb/Rec@bb8f40ed
Milestone changed to %v28r0
Reassigned to @clemenci
- Resolved by Marco Clemencic
- Resolved by Marco Clemencic
It is not clear to me why std::optional wouldn't just do the job. There is no need to reinvent the wheel if there isn't a striking reason against it. Gcc49 already has it in the experimental namespace so no problem in picking it up. This long thread already tells me that doing something consistent is non-trivial.
Added 1 commit:
- 61f75829 - reverting to original submission (no compact_optional)
mentioned in commit 0eb189a8
mentioned in merge request atlas/Gaudi!99 (merged)
mentioned in commit atlas/Gaudi@d710ddcd
mentioned in commit atlas/Gaudi@1a46d24c