【问题标题】:Boost::Spirit parser: Looking for max performance and min mem usageBoost::Spirit 解析器:寻找最大性能和最小内存使用量
【发布时间】:2018-09-09 08:03:56
【问题描述】:

在关于解析复杂日志的几个问题之后,我终于被告知了这样做的最佳方法。

现在的问题是是否有某种方法可以提高性能和/或减少内存使用,甚至是编译时间。我会要求答案满足这些限制:

  1. MS VS 2010(不完全是 c++11,只是实现了一些功能:auto、lambdas...)和 boost 1.53(这里唯一的问题是 string_view 仍然不可用,但它是使用string_ref 仍然有效,即使指出它将来可能会被弃用)。

  2. 日志被压缩,并使用一个开放的库直接解压缩到 RAM 内存,该库输出一个旧的原始 C“char”数组,因此不值得使用 std::string,因为内存已经分配给图书馆。它们有数千个,它们填充了几个 GB,因此不能将它们保存在内存中。我的意思是使用string_view是不可能的,因为在解析后删除了日志。

  3. 将日期字符串解析为 POSIX 时间可能是个好主意。只有两点:避免为此分配字符串应该很有趣,据我所知,POSIX 时代不承认 ms,因此它们应该保存在另一个额外的变量中。

  4. 日志中重复了一些字符串(道路变量 p.e.)。使用一些享元模式(它的 boost 实现)来减少内存可能会很有趣,即使记住这会降低性能。

  5. 使用模板库时,编译时间很痛苦。我真的很感激任何有助于减少它们的调整:也许将语法分成子语法?也许使用预编译的头文件?

  6. 它的最终用途是查询任何事件,例如获取所有 GEAR 事件(值和时间),并在固定间隔内或每次事件发生时记录所有汽车变量。日志中有两种类型的记录:纯“位置”记录和“位置+事件”记录(我的意思是,每次解析事件时也必须保存位置)。将它们分成两个向量可以实现快速查询,但会减慢解析速度。仅使用公共向量可以快速解析,但会减慢查询速度。对此有任何想法吗?也许像之前建议的那样,提升多个索引容器会有所帮助?

请不要犹豫,提供任何解决方案或更改您认为可能有助于实现目标的任何内容。

//#define BOOST_SPIRIT_DEBUG
#include <boost/fusion/adapted/struct.hpp>
#include <boost/spirit/include/qi.hpp>
#include <cstring> // strlen

typedef char const* It;

namespace MyEvents {
    enum Kind { LOCATION, SLOPE, GEAR, DIR };

    struct Event {
        Kind kind;
        double value;
    };

    struct LogRecord {
        int driver;        
        double time;
        double vel;
        double km;
        std::string date;
        std::string road;
        Event event;
    };

    typedef std::vector<LogRecord> LogRecords;
}

BOOST_FUSION_ADAPT_STRUCT(MyEvents::Event,
    (MyEvents::Kind, kind)
    (double, value))


BOOST_FUSION_ADAPT_STRUCT(MyEvents::LogRecord,
        (std::string, date)
        (double, time)
        (int, driver)
        (double, vel)
        (std::string, road)
        (double, km)
        (MyEvents::Event, event))

namespace qi = boost::spirit::qi;

namespace QiParsers {
    template <typename It>
    struct LogParser : qi::grammar<It, MyEvents::LogRecords()> {

        LogParser() : LogParser::base_type(start) {
            using namespace qi;

            kind.add
                ("SLOPE", MyEvents::SLOPE)
                ("GEAR", MyEvents::GEAR)
                ("DIR", MyEvents::DIR);

            values.add("G1", 1.0)
                      ("G2", 2.0)
                      ("REVERSE", -1.0)
                      ("NORTH", 1.0)
                      ("EAST", 2.0)
                      ("WEST", 3.0)
                      ("SOUTH", 4.0);

            MyEvents::Event null_event = {MyEvents::LOCATION, 0.0};

            line_record
                = '[' >> raw[repeat(4)[digit] >> '-' >> repeat(3)[alpha] >> '-' >> repeat(2)[digit] >> ' ' >> 
                             repeat(2)[digit] >> ':' >> repeat(2)[digit] >> ':' >> repeat(2)[digit] >> '.' >> repeat(6)[digit]] >> "]"
                >> " - " >> double_ >> " s"
                >> " => Driver: "  >> int_
                >> " - Speed: "    >> double_
                >> " - Road: "     >> raw[+graph]
                >> " - Km: "       >> double_
                >> (" - " >> kind >> ": " >> (double_ | values) | attr(null_event));

            start = line_record % eol;

            //BOOST_SPIRIT_DEBUG_NODES((start)(line_record))
        }

      private:
        qi::rule<It, MyEvents::LogRecords()> start;

        qi::rule<It, MyEvents::LogRecord()> line_record;

        qi::symbols<char, MyEvents::Kind> kind;
        qi::symbols<char, double> values;
    };
}

MyEvents::LogRecords parse_spirit(It b, It e) {
    static QiParsers::LogParser<It> const parser;

    MyEvents::LogRecords records;
    parse(b, e, parser, records);

    return records;
}

static char input[] = 
"[2018-Mar-13 13:13:59.580482] - 0.200 s => Driver: 0 - Speed: 0.0 - Road: A-11 - Km: 90.0 - SLOPE: 5.5\n\
[2018-Mar-13 13:14:01.170203] - 1.790 s => Driver: 0 - Speed: 0.0 - Road: A-11 - Km: 90.0 - GEAR: G1\n\
[2018-Mar-13 13:14:01.170203] - 1.790 s => Driver: 0 - Speed: 0.0 - Road: A-11 - Km: 90.0 - DIR: NORTH\n\
[2018-Mar-13 13:14:01.170203] - 1.790 s => Driver: 0 - Speed: 0.1 - Road: A-11 - Km: 90.0\n\
[2018-Mar-13 13:14:01.170203] - 1.980 s => Driver: 0 - Speed: 0.0 - Road: A-11 - Km: 90.1 - GEAR: G2\n\
[2018-Mar-13 13:14:01.819966] - 2.440 s => Driver: 0 - Speed: 0.1 - Road: B-16 - Km: 90.2\n\
[2018-Mar-13 13:14:01.819966] - 2.440 s => Driver: 0 - Speed: 0.1 - Road: B-16 - Km: 90.2 - DIR: EAST\n\
[2018-Mar-13 13:15:01.819966] - 3.440 s => Driver: 0 - Speed: 0.2 - Road: B-16 - Km: 90.3 - SLOPE: -10\n\
[2018-Mar-13 13:14:01.170203] - 1.980 s => Driver: 0 - Speed: 0.0 - Road: B-16 - Km: 90.4 - GEAR: REVERSE\n";
static const size_t len = strlen(input);

namespace MyEvents { // for debug/demo
    using boost::fusion::operator<<;

    static inline std::ostream& operator<<(std::ostream& os, Kind k) {
        switch(k) {
            case LOCATION: return os << "LOCATION";
            case SLOPE:    return os << "SLOPE";
            case GEAR:     return os << "GEAR";
            case DIR:      return os << "DIR";
        }
        return os;
    }
}

int main() {
    MyEvents::LogRecords records = parse_spirit(input, input+len);
    std::cout << "Parsed: " << records.size() << " records\n";

    for (MyEvents::LogRecords::const_iterator it = records.begin(); it != records.end(); ++it)
        std::cout << *it << "\n"; 

    return 0;
}

【问题讨论】:

    标签: boost boost-spirit


    【解决方案1】:

    是的string_ref 本质上是相同的,但在某些时候使用的界面与std::string_view 略有不同

    修订 1:POSIX 时间

    事实证明,存储 POSIX 时间非常简单:

    #include <boost/date_time/posix_time/posix_time_io.hpp>
    

    接下来,替换类型:

    typedef boost::posix_time::ptime Timestamp;
    
    struct LogRecord {
        int driver;
        double time;
        double vel;
        double km;
        Timestamp date;    // << HERE using Timestamp now
        std::string road;
        Event event;
    };
    

    并将解析器简化为:

    '[' >> stream >> ']'
    

    打印Live On Coliru

    Parsed: 9 records
    (2018-Mar-13 13:13:59.580482 0.2 0 0 A-11 90 (SLOPE 5.5))
    (2018-Mar-13 13:14:01.170203 1.79 0 0 A-11 90 (GEAR 1))
    (2018-Mar-13 13:14:01.170203 1.79 0 0 A-11 90 (DIR 1))
    (2018-Mar-13 13:14:01.170203 1.79 0 0.1 A-11 90 (LOCATION 0))
    (2018-Mar-13 13:14:01.170203 1.98 0 0 A-11 90.1 (GEAR 2))
    (2018-Mar-13 13:14:01.819966 2.44 0 0.1 B-16 90.2 (LOCATION 0))
    (2018-Mar-13 13:14:01.819966 2.44 0 0.1 B-16 90.2 (DIR 2))
    (2018-Mar-13 13:15:01.819966 3.44 0 0.2 B-16 90.3 (SLOPE -10))
    (2018-Mar-13 13:14:01.170203 1.98 0 0 B-16 90.4 (GEAR -1))
    

    修订 #2:压缩文件

    您也可以使用 IOStreams 透明地解压缩输入:

    int main(int argc, char **argv) {
        MyEvents::LogRecords records;
    
        for (char** arg = argv+1; *arg && (argv+argc != arg); ++arg) {
            bool ok = parse_logfile(*arg, records);
    
            std::cout 
                << "Parsing " << *arg << (ok?" - success" : " - errors")
                << " (" << records.size() << " records total)\n";
        }
    
        for (MyEvents::LogRecords::const_iterator it = records.begin(); it != records.end(); ++it)
            std::cout << *it << "\n"; 
    }
    

    parse_logfile 那么可以实现为:

    template <typename It>
    bool parse_spirit(It b, It e, MyEvents::LogRecords& into) {
        static QiParsers::LogParser<It> const parser;
    
        return parse(b, e, parser, into);
    }
    
    bool parse_logfile(char const* fname, MyEvents::LogRecords& into) {
        boost::iostreams::filtering_istream is;
        is.push(boost::iostreams::gzip_decompressor());
    
        std::ifstream ifs(fname, std::ios::binary);
        is.push(ifs);
    
        boost::spirit::istream_iterator f(is >> std::noskipws), l;
        return parse_spirit(f, l, into);
    }
    

    注意:该库具有 zlib、gzip 和 bzip2 解压缩器。我选择 gzip 进行演示

    打印Live On Coliru

    Parsing input.gz - success (9 records total)
    (2018-Mar-13 13:13:59.580482 0.2 0 0 A-11 90 (SLOPE 5.5))
    (2018-Mar-13 13:14:01.170203 1.79 0 0 A-11 90 (GEAR 1))
    (2018-Mar-13 13:14:01.170203 1.79 0 0 A-11 90 (DIR 1))
    (2018-Mar-13 13:14:01.170203 1.79 0 0.1 A-11 90 (LOCATION 0))
    (2018-Mar-13 13:14:01.170203 1.98 0 0 A-11 90.1 (GEAR 2))
    (2018-Mar-13 13:14:01.819966 2.44 0 0.1 B-16 90.2 (LOCATION 0))
    (2018-Mar-13 13:14:01.819966 2.44 0 0.1 B-16 90.2 (DIR 2))
    (2018-Mar-13 13:15:01.819966 3.44 0 0.2 B-16 90.3 (SLOPE -10))
    (2018-Mar-13 13:14:01.170203 1.98 0 0 B-16 90.4 (GEAR -1))
    

    修订 3:字符串实习

    “Interned”字符串或“Atoms”是减少字符串分配的常用方法。您可以使用 Boost Flyweight,但根据我的经验,正确操作有点复杂。那么,为什么不创建自己的抽象:

    struct StringTable {
        typedef boost::string_ref Atom;
        typedef boost::container::flat_set<Atom> Index;
        typedef std::deque<char> Store;
    
        /* An insert in the middle of the deque invalidates all the iterators and
         * references to elements of the deque. An insert at either end of the
         * deque invalidates all the iterators to the deque, but has no effect on
         * the validity of references to elements of the deque.
         */
        Store backing;
        Index index;
    
        Atom intern(boost::string_ref const& key) {
            Index::const_iterator it = index.find(key);
    
            if (it == index.end()) {
                Store::const_iterator match = std::search(
                        backing.begin(), backing.end(),
                        key.begin(), key.end());
    
                if (match == backing.end()) {
                    size_t offset = backing.size();
                    backing.insert(backing.end(), key.begin(), key.end());
                    match = backing.begin() + offset;
                }
    
                it = index.insert(Atom(&*match, key.size())).first;
            }
            // return the Atom from backing store
            return *it;
        }
    };
    

    现在,我们需要将其集成到解析器中。我建议使用语义操作

    注意:特征在这里仍然可以提供帮助,但它们是静态的,这需要 StringTable 是全局的,这是我永远不会做出的选择...除非绝对有义务

    首先,改变 Ast:

    struct LogRecord {
        int driver;
        double time;
        double vel;
        double km;
        Timestamp date;
        Atom road;       // << HERE using Atom now
        Event event;
    };
    

    接下来,让我们创建一个规则来创建这样一个原子:

    qi::rule<It, MyEvents::Atom()> atom;
    
    atom = raw[+graph][_val = intern_(_1)];
    

    当然,这就引出了语义动作是如何实现的问题:

    struct intern_f {
        StringTable& _table;
    
        typedef StringTable::Atom result_type;
        explicit intern_f(StringTable& table) : _table(table) {}
    
        StringTable::Atom operator()(boost::iterator_range<It> const& range) const {
            return _table.intern(sequential(range));
        }
    
      private:
        // be more efficient if It is const char*
        static boost::string_ref sequential(boost::iterator_range<const char*> const& range) {
            return boost::string_ref(range.begin(), range.size());
        }
        template <typename OtherIt>
        static std::string sequential(boost::iterator_range<OtherIt> const& range) {
            return std::string(range.begin(), range.end());
        }
    };
    boost::phoenix::function<intern_f> intern_;
    

    语法的构造函数将intern_ 函子连接到传入的StringTable&amp;

    完整演示

    Live On Coliru

    //#define BOOST_SPIRIT_DEBUG
    #include <boost/fusion/adapted/struct.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    #include <boost/date_time/posix_time/posix_time_io.hpp>
    #include <boost/iostreams/filtering_stream.hpp>
    #include <boost/iostreams/filter/gzip.hpp>
    #include <boost/utility/string_ref.hpp>
    #include <boost/container/flat_set.hpp>
    #include <fstream>
    #include <cstring> // strlen
    
    struct StringTable {
        typedef boost::string_ref Atom;
        typedef boost::container::flat_set<Atom> Index;
        typedef std::deque<char> Store;
    
        /* An insert in the middle of the deque invalidates all the iterators and
         * references to elements of the deque. An insert at either end of the
         * deque invalidates all the iterators to the deque, but has no effect on
         * the validity of references to elements of the deque.
         */
        Store backing;
        Index index;
    
        Atom intern(boost::string_ref const& key) {
            Index::const_iterator it = index.find(key);
    
            if (it == index.end()) {
                Store::const_iterator match = std::search(
                        backing.begin(), backing.end(),
                        key.begin(), key.end());
    
                if (match == backing.end()) {
                    size_t offset = backing.size();
                    backing.insert(backing.end(), key.begin(), key.end());
                    match = backing.begin() + offset;
                }
    
                it = index.insert(Atom(&*match, key.size())).first;
            }
            // return the Atom from backing store
            return *it;
        }
    };
    
    namespace MyEvents {
        enum Kind { LOCATION, SLOPE, GEAR, DIR };
    
        struct Event {
            Kind kind;
            double value;
        };
    
        typedef boost::posix_time::ptime Timestamp;
        typedef StringTable::Atom Atom;
    
        struct LogRecord {
            int driver;
            double time;
            double vel;
            double km;
            Timestamp date;
            Atom road;
            Event event;
        };
    
        typedef std::vector<LogRecord> LogRecords;
    }
    
    BOOST_FUSION_ADAPT_STRUCT(MyEvents::Event,
            (MyEvents::Kind, kind)
            (double, value))
    
    BOOST_FUSION_ADAPT_STRUCT(MyEvents::LogRecord,
            (MyEvents::Timestamp, date)
            (double, time)
            (int, driver)
            (double, vel)
            (MyEvents::Atom, road)
            (double, km)
            (MyEvents::Event, event))
    
    namespace qi = boost::spirit::qi;
    
    namespace QiParsers {
        template <typename It>
        struct LogParser : qi::grammar<It, MyEvents::LogRecords()> {
    
            LogParser(StringTable& strings) : LogParser::base_type(start), intern_(intern_f(strings)) {
                using namespace qi;
    
                kind.add
                    ("SLOPE", MyEvents::SLOPE)
                    ("GEAR", MyEvents::GEAR)
                    ("DIR", MyEvents::DIR);
    
                values.add("G1", 1.0)
                          ("G2", 2.0)
                          ("REVERSE", -1.0)
                          ("NORTH", 1.0)
                          ("EAST", 2.0)
                          ("WEST", 3.0)
                          ("SOUTH", 4.0);
    
                MyEvents::Event null_event = {MyEvents::LOCATION, 0.0};
    
                atom = raw[+graph][_val = intern_(_1)];
    
                line_record
                    = '[' >> stream >> ']'
                    >> " - " >> double_ >> " s"
                    >> " => Driver: "  >> int_
                    >> " - Speed: "    >> double_
                    >> " - Road: "     >> atom
                    >> " - Km: "       >> double_
                    >> (" - " >> kind >> ": " >> (double_ | values) | attr(null_event));
    
                start = line_record % eol;
    
                BOOST_SPIRIT_DEBUG_NODES((start)(line_record)(atom))
            }
    
          private:
            struct intern_f {
                StringTable& _table;
    
                typedef StringTable::Atom result_type;
                explicit intern_f(StringTable& table) : _table(table) {}
    
                StringTable::Atom operator()(boost::iterator_range<It> const& range) const {
                    return _table.intern(sequential(range));
                }
    
              private:
                // be more efficient if It is const char*
                static boost::string_ref sequential(boost::iterator_range<const char*> const& range) {
                    return boost::string_ref(range.begin(), range.size());
                }
                template <typename OtherIt>
                static std::string sequential(boost::iterator_range<OtherIt> const& range) {
                    return std::string(range.begin(), range.end());
                }
            };
            boost::phoenix::function<intern_f> intern_;
    
            qi::rule<It, MyEvents::LogRecords()> start;
    
            qi::rule<It, MyEvents::LogRecord()> line_record;
            qi::rule<It, MyEvents::Atom()> atom;
    
            qi::symbols<char, MyEvents::Kind> kind;
            qi::symbols<char, double> values;
        };
    }
    
    template <typename It>
    bool parse_spirit(It b, It e, MyEvents::LogRecords& into, StringTable& strings) {
        QiParsers::LogParser<It> parser(strings); // TODO optimize by not reconstructing all parser rules each time
    
        return parse(b, e, parser, into);
    }
    
    bool parse_logfile(char const* fname, MyEvents::LogRecords& into, StringTable& strings) {
        boost::iostreams::filtering_istream is;
        is.push(boost::iostreams::gzip_decompressor());
    
        std::ifstream ifs(fname, std::ios::binary);
        is.push(ifs);
    
        boost::spirit::istream_iterator f(is >> std::noskipws), l;
        return parse_spirit(f, l, into, strings);
    }
    
    namespace MyEvents { // for debug/demo
        using boost::fusion::operator<<;
    
        static inline std::ostream& operator<<(std::ostream& os, Kind k) {
            switch(k) {
                case LOCATION: return os << "LOCATION";
                case SLOPE:    return os << "SLOPE";
                case GEAR:     return os << "GEAR";
                case DIR:      return os << "DIR";
            }
            return os;
        }
    }
    
    int main(int argc, char **argv) {
        StringTable strings;
        MyEvents::LogRecords records;
    
        for (char** arg = argv+1; *arg && (argv+argc != arg); ++arg) {
            bool ok = parse_logfile(*arg, records, strings);
    
            std::cout 
                << "Parsing " << *arg << (ok?" - success" : " - errors")
                << " (" << records.size() << " records total)\n";
        }
    
        for (MyEvents::LogRecords::const_iterator it = records.begin(); it != records.end(); ++it)
            std::cout << *it << "\n"; 
    
        std::cout << "Interned strings: " << strings.index.size() << "\n";
        std::cout << "Table backing: '";
        std::copy(strings.backing.begin(), strings.backing.end(), std::ostreambuf_iterator<char>(std::cout));
        std::cout << "'\n";
        for (StringTable::Index::const_iterator it = strings.index.begin(); it != strings.index.end(); ++it) {
            std::cout << " entry - " << *it << "\n";
        }
    }
    

    当使用 2 个输入文件运行时,第二个与第一个略有不同:

    zcat input.gz | sed 's/[16] - Km/ - Km/' | gzip > second.gz
    

    打印出来

    Parsing input.gz - success (9 records total)
    Parsing second.gz - success (18 records total)
    (2018-Mar-13 13:13:59.580482 0.2 0 0 A-11 90 (SLOPE 5.5))
    (2018-Mar-13 13:14:01.170203 1.79 0 0 A-11 90 (GEAR 1))
    (2018-Mar-13 13:14:01.170203 1.79 0 0 A-11 90 (DIR 1))
    (2018-Mar-13 13:14:01.170203 1.79 0 0.1 A-11 90 (LOCATION 0))
    (2018-Mar-13 13:14:01.170203 1.98 0 0 A-11 90.1 (GEAR 2))
    (2018-Mar-13 13:14:01.819966 2.44 0 0.1 B-16 90.2 (LOCATION 0))
    (2018-Mar-13 13:14:01.819966 2.44 0 0.1 B-16 90.2 (DIR 2))
    (2018-Mar-13 13:15:01.819966 3.44 0 0.2 B-16 90.3 (SLOPE -10))
    (2018-Mar-13 13:14:01.170203 1.98 0 0 B-16 90.4 (GEAR -1))
    (2018-Mar-13 13:13:59.580482 0.2 0 0 A-1 90 (SLOPE 5.5))
    (2018-Mar-13 13:14:01.170203 1.79 0 0 A-1 90 (GEAR 1))
    (2018-Mar-13 13:14:01.170203 1.79 0 0 A-1 90 (DIR 1))
    (2018-Mar-13 13:14:01.170203 1.79 0 0.1 A-1 90 (LOCATION 0))
    (2018-Mar-13 13:14:01.170203 1.98 0 0 A-1 90.1 (GEAR 2))
    (2018-Mar-13 13:14:01.819966 2.44 0 0.1 B-1 90.2 (LOCATION 0))
    (2018-Mar-13 13:14:01.819966 2.44 0 0.1 B-1 90.2 (DIR 2))
    (2018-Mar-13 13:15:01.819966 3.44 0 0.2 B-1 90.3 (SLOPE -10))
    (2018-Mar-13 13:14:01.170203 1.98 0 0 B-1 90.4 (GEAR -1))
    

    有趣的是在interned string stats中:

    Interned strings: 4
    Table backing: 'A-11B-16'
     entry - A-1
     entry - A-11
     entry - B-1
     entry - B-16
    

    请注意 B-1A-1 是如何作为 A-11B-16 的子字符串进行重复数据删除的,因为它们已经被实习了。预置字符串表可能有助于实现最佳重用。

    各种备注

    我没有很多减少编译时间的技巧。我只是将所有 Spirit 的东西放在一个单独的 TU 中,并接受那个的编译时间。毕竟,这是关于用编译时间换取运行时性能。

    关于字符串实习,您最好使用flat_set&lt;char const*&gt;,以便您只根据需要构造具有特定长度的原子。

    如果所有字符串都很小,那么只使用小字符串优化可能会(远)更好。

    我会让您进行比较基准测试,您可能希望继续使用自己的解压缩 + const char* 迭代器。这主要是为了表明 Boost 有它,您不需要“一次读取整个文件”。

    事实上,在这个问题上,您可能希望将结果存储在内存映射文件中,这样即使超出物理内存限制,您也可以愉快地工作。

    多索引和查询

    你可以在我之前的回答中找到具体的例子:BONUS: Multi-Index

    特别注意通过引用获取索引的方式:

    Indexing::Table idx(events.begin(), events.end());
    

    这也可用于将结果集存储在另一个(索引)容器中以进行重复/进一步处理。

    【讨论】:

    • 对您使用正确的 boost 库所能实现的一切的精彩解释和全面阐述。我想知道您是否参与了 ​​boost::spirit 的开发或维护。再次感谢您的回答(也感谢您提供免费的英语课程)。
    • 也许我在强加,所以如果您愿意,我可以为接下来的两个疑问打开一个新问题。 1) date_time 是少数需要预编译的 boost 库之一。无论如何这没什么大不了的,但我想知道使用其他 lib 或只是旧的 time_t 是否容易,即使知道它的局限性并且它不是 posix。 2)如果我尝试将“Event”结构成员添加到“LogRecord”结构中(只留下一个结构),我发现我不能使用“attr(MyEvents :: LOCATION)>> attr(0.0)”来赋予值默认情况下(仅匹配 LOCATION,但不匹配 0.0)。有什么解决办法吗?
    • 你可以“伪造”:gist.github.com/sehe/212ce5e3086eb3b26a6e6f806002f967/revisions 使用 c++11 get_time。请注意,Timestamp 现在是原来的两倍。有关替代方案,请参见例如stackoverflow.com/questions/37856887/…
    • 现在还添加了将Event 扁平化为LogRecord 的修订版,以及Live On Coliru。如果您对此还有其他问题,我认为是时候提出一个新的(有针对性的)问题了
    • 我认为没有必要提出新问题。您提供的源代码清晰易懂。感谢您的所有帮助和关注。
    猜你喜欢
    • 1970-01-01
    • 2015-09-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-15
    • 2012-11-26
    • 2021-01-03
    • 2019-07-16
    相关资源
    最近更新 更多