[glibmm/wip/dboles/ustring-sprintf] ustring: Add sprintf(), wrapping g_strdup_printf()



commit 34c666c46a13fa8f322f29e9b31cf50f337624b5
Author: Daniel Boles <dboles src gnome org>
Date:   Tue Jun 18 19:28:34 2019 +0100

    ustring: Add sprintf(), wrapping g_strdup_printf()
    
    Add another way to produce formatted ustrings, this time using printf
    syntax, by forwarding arguments to g_strdup_printf() and then copying
    the result into the returned ustring.
    
    This includes a private ustring::sprintify() function that by default
    just forward its argument but can be overloaded to do something else.
    In this commit, that is overloaded for ustring and std::string so that
    their .c_str() is passed to printf instead, avoiding the ugliness of
    users always having to write .c_str() in their own lists of arguments.
    
    Note that the same lack of type safety as plagues printf() and all its
    variants (in both C and GLib) applies here: the arguments are just
    forwarded on, so if you include too few or the wrong types for the
    placeholders you specify, you invoke undefined behaviour just as in C.
    
    For reasons like that, C++'s preference of streams over stdio, and the
    hope that we'll eventually get an actual nice string-formatting solution
    in the C++ Standard, I don't go out of my way to shout about this in the
    documentation. Users who really want sprintf() will find it, without us
    having to shout too loudly about it and risk being seen as recommending
    it more than anything else. It's here for those who know they need it.
    
    https://gitlab.gnome.org/GNOME/glibmm/issues/21

 glib/glibmm/ustring.h                | 91 ++++++++++++++++++++++++++++++++++++
 tests/Makefile.am                    |  2 +
 tests/glibmm_ustring_sprintf/main.cc | 55 ++++++++++++++++++++++
 3 files changed, 148 insertions(+)
---
diff --git a/glib/glibmm/ustring.h b/glib/glibmm/ustring.h
index edd713db..b7d0de7a 100644
--- a/glib/glibmm/ustring.h
+++ b/glib/glibmm/ustring.h
@@ -714,6 +714,54 @@ public:
   template <class... Ts>
   static inline ustring format(const Ts&... args);
 
+  /*! Substitute placeholders in a format string with the referenced arguments.
+   *
+   * This function takes a template string in the format used by C’s
+   * <tt>printf()</tt> family of functions and an arbitrary number of arguments,
+   * replaces each placeholder in the template with the formatted version of its
+   * corresponding argument at the same ordinal position in the list of
+   * subsequent arguments, and returns the result in a new Glib::ustring.
+   *
+   * Note: You must pass the correct number/types/order of arguments to match
+   * the format string, as when calling <tt>printf()</tt> directly. glibmm does
+   * not check this for you. Breaking this contract invokes undefined behavior.
+   *
+   * The exception is that glibmm special-cases std::string and Glib::ustring,
+   * so you can pass them in positions corresponding to <tt>%s</tt> placeholders
+   * without having to call their .c_str() functions; glibmm does that for you.
+   *
+   * Said restriction also makes sprintf() unsuitable for translatable strings,
+   * as translators cannot reorder the placeholders to suit their language. If
+   * you wish to support translation, you should instead use compose(), as its
+   * placeholders are numbered rather than ordinal, so they can be moved freely.
+   *
+   * @par Example:
+   * @code
+   *
+   * const auto greeting = std::string{"Hi"};
+   * const auto name = Glib::ustring{"Dennis"};
+   * const auto your_cows = 3;
+   * const auto my_cows = 11;
+   * const auto cow_percentage = 100.0 * your_cows / my_cows;
+   *
+   * const auto text = Glib::ustring::sprintf(
+   *   "%s, %s! You have %d cows. That's about %0.2f%% of the %d cows I have.",
+   *   greeting, name, your_cows, cow_percentage, my_cows);
+   *
+   * std::cout << text;
+   * // Hi, Dennis! You have 2 cows. That's about 27.27% of the 11 cows I have.
+   * @endcode
+   *
+   * @param fmt The template string, in the format used by <tt>printf()</tt> et al.
+   * @param args A set of arguments having the number and types required by @a fmt.
+   *
+   * @return The substituted message string.
+   *
+   * @newin{2,56}
+   */
+  template <class... Ts>
+  static inline ustring sprintf(const ustring& fmt, const Ts&... args);
+
   //! @}
 
 private:
@@ -740,6 +788,10 @@ private:
 
   class FormatStream;
 
+  template<class T> static inline const T& sprintify(const T& arg);
+  static inline const char* sprintify(const ustring& arg);
+  static inline const char* sprintify(const std::string& arg);
+
 #endif /* DOXYGEN_SHOULD_SKIP_THIS */
 
   std::string string_;
@@ -1156,6 +1208,33 @@ public:
   inline const ustring& ref() const { return string_; }
 };
 
+/* These helper functions used by ustring::sprintf() let users pass C++ strings
+ * to match %s placeholders, without the hassle of writing .c_str() in user code
+ */
+template<typename T>
+inline // static
+  const T&
+  ustring::sprintify(const T& arg)
+{
+  return arg;
+}
+
+inline // static
+  const char*
+  ustring::sprintify(const ustring& arg)
+{
+  return arg.c_str();
+}
+
+inline // static
+  const char*
+  ustring::sprintify(const std::string& arg)
+{
+  return arg.c_str();
+}
+
+// Public methods
+
 inline // static
   ustring
   ustring::compose(const ustring& fmt)
@@ -1174,6 +1253,18 @@ inline // static
   return compose_private(fmt, {&Stringify<Ts>(args).ref()...});
 }
 
+template <class... Ts>
+inline // static
+  ustring
+  ustring::sprintf(const ustring& fmt, const Ts&... args)
+{
+  auto c_str = g_strdup_printf(fmt.c_str(), sprintify(args)...);
+  Glib::ustring ustr(c_str);
+  g_free(c_str);
+
+  return ustr;
+}
+
 #endif /* DOXYGEN_SHOULD_SKIP_THIS */
 
 /** @relates Glib::ustring */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 339eeada..3f6f81c9 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -40,6 +40,7 @@ check_PROGRAMS =                              \
        glibmm_objectbase_move/test                     \
        glibmm_ustring_compose/test             \
        glibmm_ustring_format/test              \
+       glibmm_ustring_sprintf/test             \
        glibmm_value/test                       \
        glibmm_variant/test                     \
        glibmm_vector/test                      \
@@ -113,6 +114,7 @@ glibmm_objectbase_move_test_SOURCES      = glibmm_objectbase_move/main.cc \
                                           glibmm_object/test_derived_object.h
 glibmm_ustring_compose_test_SOURCES      = glibmm_ustring_compose/main.cc
 glibmm_ustring_format_test_SOURCES       = glibmm_ustring_format/main.cc
+glibmm_ustring_sprintf_test_SOURCES      = glibmm_ustring_sprintf/main.cc
 glibmm_value_test_SOURCES                = glibmm_value/main.cc
 glibmm_variant_test_SOURCES              = glibmm_variant/main.cc
 glibmm_vector_test_SOURCES               = glibmm_vector/main.cc
diff --git a/tests/glibmm_ustring_sprintf/main.cc b/tests/glibmm_ustring_sprintf/main.cc
new file mode 100644
index 00000000..232be152
--- /dev/null
+++ b/tests/glibmm_ustring_sprintf/main.cc
@@ -0,0 +1,55 @@
+#include <glibmm/init.h>
+#include <glibmm/ustring.h>
+
+#include <cstdlib>
+#include <iostream>
+
+namespace {
+
+template <class... Ts>
+void
+test(const Glib::ustring& expected, const Glib::ustring& fmt, const Ts&... ts)
+{
+  const auto actual = Glib::ustring::sprintf(fmt, ts...);
+
+  if (actual != expected)
+  {
+    std::cerr << "error testing Glib::ustring::sprintf():\n"
+      "expected (" << expected.size() << "):\n" << expected << "\n\n"
+      "actual   (" << actual  .size() << "):\n" << actual   << "\n";
+
+    std::exit(EXIT_FAILURE);
+  }
+}
+
+} // anonymous namespace
+
+int
+main(int, char**)
+{
+  // Don't use the user's preferred locale. The decimal delimiter may be ','
+  // instead of the expected '.'.
+  Glib::set_init_to_users_preferred_locale(false);
+
+  Glib::init();
+
+  test("No formatting here, just a boring string",
+       "No formatting here, just a boring string");
+
+  test("Interpolating another string: \"here it is\" and there it was gone.",
+       "Interpolating another string: \"%s\" and there it was gone.", "here it is");
+
+  test("some stuff and then an int: 42",
+       "some stuff and then an int: %d", 42);
+
+  const auto greeting = std::string{"Hi"};
+  const auto name = Glib::ustring{"Dennis"};
+  const auto your_cows = 3;
+  const auto my_cows = 11;
+  const auto cow_percentage = 100.0 * your_cows / my_cows;
+  test("Hi, Dennis! You have 3 cows.\nThat's about 27.27% of the 11 cows I have.",
+       "%s, %s! You have %d cows.\nThat's about %0.2f%% of the %d cows I have.",
+       greeting, name, your_cows, cow_percentage, my_cows);
+
+  return EXIT_SUCCESS;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]