为什么(或何时)模板引入其名称空间?

Why (or when) does a template bring in its namespace?

这是一个例子:

#define MAKE_IT_WORK false
namespace Bob {  // Bob's project namespace
   struct DeviceFrequency {};
   extern void debugf(const char* fmt, ...);
}  // namespace Bob

namespace SSN {  // Super-Secret Namespace
   namespace debugging {
     extern int  ssn_debug;
     extern void debugf(const char* fmt, ...);
   }  // namespace debugging
}  // namespace SSN

namespace SSN::signals { // Super-Secret Namespace, project signals
   template<typename Coder> // In the example, this imports Bob's namespace
   class Frequency {
     public:
     Frequency( void )
     { using namespace ::SSN::debugging; // Why isn't this enough??
       using ::SSN::debugging::debugf;   // Or this??
       if( ssn_debug )
       #if MAKE_IT_WORK
         ::SSN::debugging::  // How can a developer predict that this is needed??
       #endif
         debugf("Frequency(%p,%zd)::Frequency\n", this, sizeof(*this));
     }
   }; // class Frequency
}  // namespace SSN::signals

struct Controller {
   SSN::signals::Frequency<Bob::DeviceFrequency> bobcon;
   Controller( void ) : bobcon() {}
}; // class Controller

在这个例子中,Bob 复制了 debugf 函数,因为他不想将整个 SSN 命名空间带入他的私有命名空间,也不想每次都为完全限定它而烦恼他用它。

SSN 开发人员没有意识到模板也可以导入其名称空间(直到它发生),显然是在使用 ADL 查找。尽管 ADL 查找指出命名空间中的 using 语句被忽略,但它为什么起作用根本没有意义。一个命名空间无意中污染另一个命名空间似乎太容易了,而且很难预测这有朝一日可能在内联代码中发生的位置。

看起来(至少)每当在命名空间中使用模板时,每个 命名空间函数引用都必须是完全限定的,因为在使用模板时您无法预测何时使用名称否则可能会发生冲突。这个对吗?如果是这样,是否有任何方法可以避免似乎需要的所有额外名称限定符键入?这种暴露仅限于模板,还是所有导入的内联代码都同样容易受到攻击?

使用 gcc 版本 10.2.0 和 10.2.1 (Red Hat 10.2.1-5) 编译 (Dirty.cpp),我收到以下消息:

make dirty 
c++  -o Dirty.o -c S/Dirty.cpp  -D_CC_GCC -D_OS_BSD -D_HW_X86 -D_OS_LINUX -IS -IH -g -O3 -finline->functions -std=gnu++17 -Wall -Wextra -Wmissing-declarations -Wswitch-default -Werror
S/Dirty.cpp: In instantiation of ‘SSN::signals::Frequency<Coder>::Frequency() [with Coder = Bob::DeviceFrequency]’:
S/Dirty.cpp:146:32:   required from here
S/Dirty.cpp:139:12: error: call of overloaded ‘debugf(const char [30], SSN::signals::Frequency<Bob::DeviceFrequency>*, long unsigned int)’ is ambiguous
 139 |      debugf("Frequency(%p,%zd)::Frequency\n", this, sizeof(*this));
     |      ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
S/Dirty.cpp:124:13: note: candidate: ‘void SSN::debugging::debugf(const char*, ...)’
 124 | extern void debugf(const char* fmt, ...);
     |             ^~~~~~
S/Dirty.cpp:117:13: note: candidate: ‘void Bob::debugf(const char*, ...)’
 117 | extern void debugf(const char* fmt, ...);
     |

(已编辑:该示例已恢复为原始版本,现在由@1201ProgramAlarm 重新格式化。修改后的版本出现在下面的部分答案中,测试示例如下。)

对于模板,ADL 包括与模板关联的命名空间和实体 为模板类型参数提供的模板参数类型。因为对 debugf 的调用包括 this 作为参数,所以 ADL 将包括 namespace Bob 因为模板是用 Bob::DeviceFrequency.

实例化的

更新示例:

// Last change: 2020/10/30 11:15 EDT
#define MAKE_IT_WORK true // Modified (Example compiles)
namespace Bob {  // Bob's project namespace
   extern double thing;     // (Added for answer testing)
   struct Bar { int foo; }; // (Added for answer testing)
   struct Foo { int bar; }; // (Added for answer testing)
   extern Foo bar;          // (Added for answer testing)
   struct DeviceFrequency { double f; }; // (Modified: added "double f;")
   extern void debugf(const char* fmt, ...);
   static inline void function(DeviceFrequency& E)  // (??TRICKY inference??)
     { debugf("default function, E: %f\n", E); }
}  // namespace Bob

namespace SSN {  // Super-Secret Namespace
   namespace debugging {
     extern int  ssn_debug;
     extern void debugf(const char* fmt, ...);
   }  // namespace debugging
}  // namespace SSN

namespace SSN::signals { // Super-Secret Namespace, project signals
   template<typename Coder>
   class Frequency {
     // Note: The Function type references the template indirectly, and has
     // no purpose other than to make sure that a <Coder> function handler
     // receives a <Coder&> parameter. ADL doesn't care. It brings in the
     // Coder's namespace looking for functions to override.
     typedef std::function<void(Coder&)> Function; // (Added) Another "gotcha"
     const Function function; // This indirectly references Coder&, thus Bob

     double thing= 543.21;   // (Added) Duplicates name in namespace Bob
     double f= 123.45;       // (Added) Duplicates name in Bob::DeviceFrequency
     Bob::Foo bar{876};      // (Added) Uses Bob namespace
     struct Bar { int foo= 0x0F00; } foo; // (Added) Duplicates a Bob struct name
     struct Derived : public Bob::Foo {}; // (Added) Uses Bob namespace
     Derived secret{{671}};  // (Added) Uses Bob namespace in derived object
     struct Container { int n= 888; Bob::Bar m{999}; }; // (Added) Uses Bob namespace
     Container content;      // (Added) Uses Bob namespace in container object

     public:
     // This constructor added to demonstrate indirect template references
     Frequency( const Function& _function ) : function(_function)
     { using namespace ::SSN::debugging; // Without this, Bob::debugf is used
       ::SSN::debugging::    // Disambiguate
       debugf("Frequency::Frequency(Coder: %p,%zd)\n", &_function, sizeof(_function));

       // Drive the function, mostly just to see that it's been coded correctly
       // Note the existence of function(DeviceFrequency& E) in namespace Bob.
       // This *looks* like it might call Bob::function, but it doesn't,
       // likely because function is an object containing an operator(), a
       // std::function container.
       Coder coder{2468}; function(coder);  // (??TRICKY?? Not ADL ambiguous)
       Coder rodoc{8642}; _function(rodoc); // (??TRICKY?? Not ADL ambiguous)
       Bob::function(coder); // Just to verify it's different
     }

     Frequency( void )       // Default constructor
     { using namespace ::SSN::debugging; // Why isn't this enough??
       using ::SSN::debugging::debugf;  // Or this??
       // Answer: When ADL comes into play, using statements are ignored.
       //         Also, without the first using, Bob::debugf would be used.
       //    (Rationale unclear, but that's the way it is.)
       if( ssn_debug )
         #if MAKE_IT_WORK
           // As explained by @1201ProgramAlarm, it's the 'this' parameter that
           // brought Bob's namespace into play for ADL name resolution.
           //    (Rationale unclear, but that's the way it is.)
           ::SSN::debugging::  // How can a developer predict that this is needed??
         #endif
         debugf("Frequency(%p,%zd)::Frequency\n", this, sizeof(*this));

       // All remaining lines in Frequency added for detailed testing ============
       const void* const that= this; // Another work-around
       debugf("Frequency(%p,%zd)::Frequency\n", that, sizeof(*this)); // (Works)

       Coder coder{3.14};    // (This is a Bob::DeviceFrequency)
       debugf("Coder.f: %f\n", coder.f); // (No ambiguity)
       Bob::debugf("Coder: %f\n", coder); // *** AMBIGUOUS debugf ***

       debugf("f: %f\n", f); // (No ambiguity)
       debugf("thing: %f\n", thing); // (No ambiguity)
       debugf("Bob::thing: %f\n", Bob::thing); // (No ambiguity)
       debugf("bar.bar: %d\n", bar.bar); // (No ambiguity)
       SSN::debugging::debugf("bar: %d\n", bar); // *** AMBIGUOUS debugf ***
       Bob::debugf("this->bar: %d\n", this->bar); // *** AMBIGUOUS debugf ***
       debugf("foo.foo: 0x%3x\n", foo.foo); // (No ambiguity)
       debugf("this->foo: 0x%3x\n", this->foo); // (No ambiguity)
       debugf("Bob::bar.bar: %d\n", Bob::bar.bar); // (No ambiguity)
       Bob::debugf("Bob::bar: %d\n", Bob::bar); // *** AMBIGUOUS debugf ***
       debugf("secret.bar: %d\n", secret.bar); // (No ambiguity)
       Bob::debugf("secret: %d\n", secret); // *** AMBIGUOUS debugf ***
       debugf("content: %d\n", content); // (No ambiguity)
       Bob::debugf("content.m: %d\n", content.m); // *** AMBIGUOUS debugf ***
       // End of added lines =====================================================
     }
   }; // class Frequency

   template<typename Coder>
   struct Ugly_fix {         // Macros stop ADL lookup, remove usings
      #define debugf ::SSN::debugging::debugf
      #define ssn_debug ::SSN::debugging::ssn_debug

      Ugly_fix( void )       // Default constructor
      {  if( ssn_debug ) debugf("Ugly_fix(%p)::Ugly_fix\n", this);
         debugf("Bob::bar: %d\n", Bob::bar);
         // Bob::debugf("Bob::bar: %d\n", Bob::bar); // Syntax error
      }

      #undef debugf          // Macros (MUST BE) private
      #undef ssn_debug
   }; // struct Ugly_fix

   class Non_template { public:
     Non_template( void )
     { using namespace ::SSN::debugging;
       if( ssn_debug )
         debugf("Non_template(%p,%zd)::Non_template\n", this, sizeof(*this));
     }
   }; // class Non_template
}  // namespace SSN::signals

// Namespace: **NONE**
static struct Handler { // Note: This actually *USES* ADL to find debugf
   void operator()(Bob::DeviceFrequency& E) { debugf("E: %f\n", E); }
}  handler; // struct Handler

struct Controller {
   SSN::signals::Frequency<Bob::DeviceFrequency> bobcon;
   SSN::signals::Frequency<Bob::DeviceFrequency> robcon;
   SSN::signals::Ugly_fix<Bob::DeviceFrequency>  ugly;
   SSN::signals::Non_template non_template;

   // Note that both Frequency constructors are used.
   Controller( void ) : bobcon(), robcon(handler), ugly(), non_template() {}
}; // class Controller

// IMPLEMENTATION ============================================================
namespace SSN {
   int debugging::ssn_debug= true;
   namespace debugging {
     void debugf(const char* fmt, ...) {
       printf("SSN: ");      // (For output disambiguation)
       va_list argptr; va_start(argptr, fmt); vprintf(fmt, argptr); va_end(argptr);
     }  // debugf
   }  // namespace debugging
}  // namespace SSN

namespace Bob {
   double thing= 1122.33;    // (Added for answer testing)
   Foo    bar{732};          // (Added for answer testing)
   void debugf(const char* fmt, ...) {
     printf("Bob: ");        // (For output disambiguation)
     va_list argptr; va_start(argptr, fmt); vprintf(fmt, argptr); va_end(argptr);
   }  // debugf
}  // namespace Bob

// TEST ======================================================================
static inline int            // Number of errors encountered
   test_namespace_glitch( void ) // Test namespace glitch
{
   int errorCount= 0;        // Number of errors encountered

   printf("\ntest_namespace_glitch\n");
   Controller controller;    // (Actually drive the constructor)

   if( true ) {
     errorCount++;           // *Why* does 'this' import namespace?
     printf("Still (partially) unexplained\n");
   }

   return errorCount;
}

(对于那些喜欢运行代码的人来说,大多数测试包装器也包含在这里。您只需要一个调用测试的主程序。)

以下是我知道需要注意的事项:

  • 使用 this 作为模板代码中的(裸)参数。 (感谢 @1201ProgramAlarm 指出此“功能”。)很容易不小心这样做,但很难弄清楚为什么会这样。
  • 引用(不受控制的)命名空间命名对象(即结构或 class)用作函数的参数 无论它是否在模板内部代码。这很难不小心做到。
  • 此引用可以是间接的,如在 Frequency(const Function&) 构造函数中,因此更容易被意外使用。

这可能是也可能不是 ADL“陷阱”的完整列表。 https://en.cppreference.com/w/cpp/language/adl 中有很多例子让我读起来头晕目眩,但工作的例子似乎通常不是你被 ADL 咬伤后可能不小心做的事情。

在我看来,ADL 不应在没有明确意图的情况下发生。应该需要一些语言关键字或信号,如 [[adl]]std::swap(obj1,obj2) 来激活它。但我想那只是我。

我不知道或理解 ADL 设计决定忽略 using 语句并将裸 this 参数视为对模板对象的引用的基本原理。我确定(好吧,希望如此)它们存在。您可能对上面 Andrew Koenig 的简短评论 A Personal Note About Argument-Dependent Lookup, found while diving into the mysteries of ADL in cppreference.com 感兴趣。

此外,郑重声明,我是 up-voting 并接受 @1201ProgramAlarm 的回答,因为他解释说 这个 参数使 ADL 起作用.在明确寻找它时,我没有找到任何说明会发生这种情况的文档。虽然它没有回答我的问题,“我如何防止这种情况发生?”没有它我无法开始。

有用的是,您收到看似无法解释的“模糊引用”错误消息,让您知道发生了一些奇怪和糟糕的事情。对于必须解决问题的人来说,太晚了,太糟糕了。对于那些不考虑 ADL 的人来说太糟糕了,他们可能不得不花费过多的时间来首先尝试隔离问题,甚至最终不得不调用 Whosebug 寻求帮助,因为 ADL 是如此 怪异。

我有一个绕过 ADL 的丑陋方法,显示在 Ugly_fix 构造函数中。通过使用 (ugh) 宏来 auto-magically 完全限定名称,不仅可以防止 ADL,而且不再需要 using 语句。这种宏必须是临时的,使用后未定义,避免宏命名空间污染。